Mongoose에서 채우기 후 쿼리


83

나는 일반적으로 Mongoose와 MongoDB를 처음 접했기 때문에 이와 같은 것이 가능한지 알아내는 데 어려움을 겪고 있습니다.

Item = new Schema({
    id: Schema.ObjectId,
    dateCreated: { type: Date, default: Date.now },
    title: { type: String, default: 'No Title' },
    description: { type: String, default: 'No Description' },
    tags: [ { type: Schema.ObjectId, ref: 'ItemTag' }]
});

ItemTag = new Schema({
    id: Schema.ObjectId,
    tagId: { type: Schema.ObjectId, ref: 'Tag' },
    tagName: { type: String }
});



var query = Models.Item.find({});

query
    .desc('dateCreated')
    .populate('tags')
    .where('tags.tagName').in(['funny', 'politics'])
    .run(function(err, docs){
       // docs is always empty
    });

더 나은 방법이 있습니까?

편집하다

혼란에 대해 사과드립니다. 내가하려는 것은 재미있는 태그 또는 정치 태그가 포함 된 모든 항목을 가져 오는 것입니다.

편집하다

where 절이없는 문서 :

[{ 
    _id: 4fe90264e5caa33f04000012,
    dislikes: 0,
    likes: 0,
    source: '/uploads/loldog.jpg',
    comments: [],
    tags: [{
        itemId: 4fe90264e5caa33f04000012,
        tagName: 'movies',
        tagId: 4fe64219007e20e644000007,
        _id: 4fe90270e5caa33f04000015,
        dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
        rating: 0,
        dislikes: 0,
        likes: 0 
    },
    { 
        itemId: 4fe90264e5caa33f04000012,
        tagName: 'funny',
        tagId: 4fe64219007e20e644000002,
        _id: 4fe90270e5caa33f04000017,
        dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
        rating: 0,
        dislikes: 0,
        likes: 0 
    }],
    viewCount: 0,
    rating: 0,
    type: 'image',
    description: null,
    title: 'dogggg',
    dateCreated: Tue, 26 Jun 2012 00:29:24 GMT 
 }, ... ]

where 절을 사용하면 빈 배열이 생성됩니다.

답변:


61

3.2보다 큰 최신 MongoDB를 사용하면 대부분의 경우에 $lookup대한 대안으로 사용할 수 있습니다 .populate(). 이것은 또한 조인 을 "에뮬레이트" 하기 위해 .populate()실제로 "다중 쿼리"를 수행하는 것과 반대로 "서버에서"조인을 실제로 수행하는 이점이 있습니다 .

그래서 .populate()입니다 하지 ㄱ 관계형 데이터베이스가 어떻게하는지의 의미에서 "참여"정말. 반면에 $lookup운영자는 실제로 서버에서 작업을 수행하며 "LEFT JOIN" 과 다소 유사합니다 .

Item.aggregate(
  [
    { "$lookup": {
      "from": ItemTags.collection.name,
      "localField": "tags",
      "foreignField": "_id",
      "as": "tags"
    }},
    { "$unwind": "$tags" },
    { "$match": { "tags.tagName": { "$in": [ "funny", "politics" ] } } },
    { "$group": {
      "_id": "$_id",
      "dateCreated": { "$first": "$dateCreated" },
      "title": { "$first": "$title" },
      "description": { "$first": "$description" },
      "tags": { "$push": "$tags" }
    }}
  ],
  function(err, result) {
    // "tags" is now filtered by condition and "joined"
  }
)

NB.collection.name여기에 실제 모델에 할당으로 MongoDB를 수집의 실제 이름 인 "문자열"로 평가한다. mongoose는 기본적으로 컬렉션 이름을 "복수화" $lookup하고 인수로 실제 MongoDB 컬렉션 이름을 필요로하기 때문에 (서버 작업이므로), 컬렉션 이름을 직접 "하드 코딩"하는 대신 mongoose 코드에서 사용하는 편리한 트릭입니다. .

$filter원하지 않는 항목을 제거하기 위해 배열을 사용할 수도 있지만 , 이는 an 및 조건이 뒤 따르는 특수 조건에 대한 집계 파이프 라인 최적화 로 인해 실제로 가장 효율적인 형식 입니다.$lookup$unwind$match

이로 인해 실제로 3 개의 파이프 라인 단계가 하나로 통합됩니다.

   { "$lookup" : {
     "from" : "itemtags",
     "as" : "tags",
     "localField" : "tags",
     "foreignField" : "_id",
     "unwinding" : {
       "preserveNullAndEmptyArrays" : false
     },
     "matching" : {
       "tagName" : {
         "$in" : [
           "funny",
           "politics"
         ]
       }
     }
   }}

이것은 실제 작업이 "먼저 조인 할 컬렉션을 필터링"한 다음 결과를 반환하고 배열을 "풀기"하므로 매우 최적입니다. 두 가지 방법이 모두 사용되므로 결과가 BSON 제한 인 16MB를 깨뜨리지 않습니다. 이는 클라이언트에없는 제약입니다.

유일한 문제는 특히 배열로 결과를 원할 때 어떤면에서 "반 직관적"으로 보이지만 $group원래 문서 양식으로 재구성되므로 여기에 해당됩니다.

또한 현재로서는 $lookup서버가 사용하는 것과 동일한 최종 구문 으로 실제로 작성할 수 없다는 것도 안타깝습니다 . IMHO, 이것은 수정해야 할 감독입니다. 그러나 현재로서는 시퀀스를 사용하는 것만으로도 효과가 있으며 최상의 성능과 확장 성을 갖춘 가장 실행 가능한 옵션입니다.

부록-MongoDB 3.6 이상

여기에 표시된 패턴 은 다른 단계가로 롤링되는 방식으로 인해 상당히 최적화 되었지만 $lookup일반적으로 두 단계 모두에 내재 된 "LEFT JOIN" $lookuppopulate()"최적의" 사용으로 인해 무효화 된다는 점에서 하나의 실패가 있습니다. $unwind여기에 빈 배열을 보존하지 않습니다. preserveNullAndEmptyArrays옵션을 추가 할 수 있지만 이는 위에서 설명한 "최적화 된" 시퀀스를 무효화 하고 기본적으로 최적화에서 일반적으로 결합되는 세 단계를 모두 그대로 둡니다.

MongoDB 3.6 은 "하위 파이프 라인"표현 을 허용하는 " 보다 표현력있는" 형태로 확장됩니다 $lookup. 이는 "LEFT JOIN"을 유지한다는 목표를 충족 할뿐만 아니라 훨씬 단순화 된 구문으로 반환 된 결과를 줄이기위한 최적의 쿼리를 허용합니다.

Item.aggregate([
  { "$lookup": {
    "from": ItemTags.collection.name,
    "let": { "tags": "$tags" },
    "pipeline": [
      { "$match": {
        "tags": { "$in": [ "politics", "funny" ] },
        "$expr": { "$in": [ "$_id", "$$tags" ] }
      }}
    ]
  }}
])

$expr(가)은 "외국"값 "로컬"값이 MongoDB를 원본으로 지금 "내부적으로"무엇을 실제로 선언과 일치하기 위해 사용하는 $lookup구문. 이 형식으로 표현함으로써 우리는 $match"하위 파이프 라인"내에서 초기 표현을 스스로 조정할 수 있습니다 .

사실, 진정한 "집계 파이프 라인"으로서 $lookup다른 관련 컬렉션에 대한 레벨 "중첩"을 포함하여이 "하위 파이프 라인"표현식 내에서 집계 파이프 라인으로 할 수있는 모든 작업을 수행 할 수 있습니다 .

추가 사용은 여기에서 묻는 질문의 범위를 약간 벗어납니다. 그러나 "중첩 된 모집단"과 관련하여의 새로운 사용 패턴은 $lookup이것이 훨씬 동일하도록 허용하고 "많은" 이 전체 사용에서 더 강력합니다.


작업 예

다음은 모델에서 정적 메서드를 사용하는 예입니다. 정적 메서드가 구현되면 호출은 다음과 같이됩니다.

  Item.lookup(
    {
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    },
    callback
  )

또는 좀 더 현대적으로 발전하는 것은 다음과 같습니다.

  let results = await Item.lookup({
    path: 'tags',
    query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
  })

.populate()구조에서 와 매우 유사 하지만 실제로는 서버에서 조인을 대신 수행합니다. 완전성을 위해 여기서 사용하면 반환 된 데이터를 부모 및 자식 사례에 따라 몽구스 문서 인스턴스로 다시 캐스팅합니다.

매우 사소하고 조정하기 쉽고 대부분의 일반적인 경우 그대로 사용하기 쉽습니다.

NB 여기서 async를 사용 하는 것은 동봉 된 예제를 간단히 실행하기위한 것입니다. 실제 구현에는 이러한 종속성이 없습니다.

const async = require('async'),
      mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.connect('mongodb://localhost/looktest');

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  dateCreated: { type: Date, default: Date.now },
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});

itemSchema.statics.lookup = function(opt,callback) {
  let rel =
    mongoose.model(this.schema.path(opt.path).caster.options.ref);

  let group = { "$group": { } };
  this.schema.eachPath(p =>
    group.$group[p] = (p === "_id") ? "$_id" :
      (p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });

  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": opt.path,
      "localField": opt.path,
      "foreignField": "_id"
    }},
    { "$unwind": `$${opt.path}` },
    { "$match": opt.query },
    group
  ];

  this.aggregate(pipeline,(err,result) => {
    if (err) callback(err);
    result = result.map(m => {
      m[opt.path] = m[opt.path].map(r => rel(r));
      return this(m);
    });
    callback(err,result);
  });
}

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

function log(body) {
  console.log(JSON.stringify(body, undefined, 2))
}
async.series(
  [
    // Clean data
    (callback) => async.each(mongoose.models,(model,callback) =>
      model.remove({},callback),callback),

    // Create tags and items
    (callback) =>
      async.waterfall(
        [
          (callback) =>
            ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
              callback),

          (tags, callback) =>
            Item.create({ "title": "Something","description": "An item",
              "tags": tags },callback)
        ],
        callback
      ),

    // Query with our static
    (callback) =>
      Item.lookup(
        {
          path: 'tags',
          query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
        },
        callback
      )
  ],
  (err,results) => {
    if (err) throw err;
    let result = results.pop();
    log(result);
    mongoose.disconnect();
  }
)

또는 async/await추가 종속성없이 Node 8.x 이상에 대해 좀 더 현대적입니다 .

const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  dateCreated: { type: Date, default: Date.now },
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});

itemSchema.statics.lookup = function(opt) {
  let rel =
    mongoose.model(this.schema.path(opt.path).caster.options.ref);

  let group = { "$group": { } };
  this.schema.eachPath(p =>
    group.$group[p] = (p === "_id") ? "$_id" :
      (p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });

  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": opt.path,
      "localField": opt.path,
      "foreignField": "_id"
    }},
    { "$unwind": `$${opt.path}` },
    { "$match": opt.query },
    group
  ];

  return this.aggregate(pipeline).exec().then(r => r.map(m => 
    this({ ...m, [opt.path]: m[opt.path].map(r => rel(r)) })
  ));
}

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

const log = body => console.log(JSON.stringify(body, undefined, 2));

(async function() {
  try {

    const conn = await mongoose.connect(uri);

    // Clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Create tags and items
    const tags = await ItemTag.create(
      ["movies", "funny"].map(tagName =>({ tagName }))
    );
    const item = await Item.create({ 
      "title": "Something",
      "description": "An item",
      tags 
    });

    // Query with our static
    const result = (await Item.lookup({
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    })).pop();
    log(result);

    mongoose.disconnect();

  } catch (e) {
    console.error(e);
  } finally {
    process.exit()
  }
})()

그리고 MongoDB 3.6 이상부터 $unwind$group빌드 없이도 :

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/looktest';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
},{ timestamps: true });

itemSchema.statics.lookup = function({ path, query }) {
  let rel =
    mongoose.model(this.schema.path(path).caster.options.ref);

  // MongoDB 3.6 and up $lookup with sub-pipeline
  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": path,
      "let": { [path]: `$${path}` },
      "pipeline": [
        { "$match": {
          ...query,
          "$expr": { "$in": [ "$_id", `$$${path}` ] }
        }}
      ]
    }}
  ];

  return this.aggregate(pipeline).exec().then(r => r.map(m =>
    this({ ...m, [path]: m[path].map(r => rel(r)) })
  ));
};

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

const log = body => console.log(JSON.stringify(body, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri);

    // Clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Create tags and items
    const tags = await ItemTag.insertMany(
      ["movies", "funny"].map(tagName => ({ tagName }))
    );

    const item = await Item.create({
      "title": "Something",
      "description": "An item",
      tags
    });

    // Query with our static
    let result = (await Item.lookup({
      path: 'tags',
      query: { 'tagName': { '$in': [ 'funny', 'politics' ] } }
    })).pop();
    log(result);


    await mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()

3
더 이상 Mongo / Mongoose를 사용하지 않지만 이것은 인기있는 질문이고 다른 사람들에게 도움이 된 것 같아서 귀하의 답변을 수락했습니다. 이 문제에 더 확장 가능한 솔루션이 있다는 것을 알게되어 기쁩니다. 업데이트 된 답변을 제공해 주셔서 감사합니다.
jschr

40

요청한 내용은 직접 지원되지 않지만 쿼리가 반환 된 후 다른 필터 단계를 추가하여 얻을 수 있습니다.

첫 번째 .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )는 태그 문서를 필터링하기 위해 수행해야하는 작업입니다. 그런 다음 쿼리가 반환 tags된 후 채우기 기준과 일치하는 문서 가없는 문서를 수동으로 필터링해야 합니다. 다음과 같이 :

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags.length;
   })
   // do stuff with docs
});

1
안녕 Aaron, 답장 주셔서 감사합니다. 내가 틀렸을 수도 있지만 populate ()의 $ in이 일치하는 태그 만 채우지 않습니까? 따라서 항목의 추가 태그는 필터링됩니다. 모든 항목을 채우고 두 번째 필터 단계에서 태그 이름을 기준으로 축소해야하는 것처럼 들립니다.
jschr

@aaronheckmann 제안 된 솔루션을 구현했지만 채우기 쿼리가 필요한 개체 만 채우지 만 여전히 전체 데이터 세트를 반환하기 때문에 .exec 이후에 필터링을 수행 할 수 있습니다. 최신 버전의 Mongoose에는 채워진 데이터 세트 만 반환하는 몇 가지 옵션이 있으므로 다른 필터링을 수행 할 필요가 없다고 생각하십니까?
Aqib Mumtaz

성능에 대해서도 알고 싶습니다. 쿼리가 마지막에 전체 데이터 세트를 반환하면 인구 필터링을 수행 할 목적이 없습니까? 당신은 무엇을 말합니까? 성능 최적화를 위해 모집단 쿼리를 조정하고 있지만 이렇게하면 대규모 데이터 세트의 성능이 향상되지 않습니까?
Aqib Mumtaz

mongoosejs.com/docs/api.html#query_Query-populate 에는 다른 사람이 관심이있는 경우 모든 세부 정보가 있습니다
samazi

채워질 때 다른 필드에서 어떻게 일치합니까?
nicogaldo

20

교체 시도

.populate('tags').where('tags.tagName').in(['funny', 'politics']) 

으로

.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )

1
답장을 보내 주셔서 감사합니다. 이것이하는 일은 각 항목을 재미 있거나 정치적인 것으로 만 채우는 것이므로 부모 목록을 줄이지 않을 것입니다. 내가 실제로 원하는 것은 태그에 재미 있거나 정치적인 항목 만있는 것입니다.
jschr

문서가 어떻게 보이는지 보여줄 수 있습니까? 태그 배열 내부의 'where'가 나에게 유효한 연산 인 것 같습니다. 구문이 잘못 되었나요? 'where'절을 완전히 제거하고 반환 된 항목이 있는지 확인 했나요? 또는 'tags.tagName'을 작성하는 것이 구문 상 괜찮은지 테스트하기 위해 잠시 동안 ref를 잊고 'Item'문서 안에 포함 된 배열로 쿼리를 시도 할 수 있습니다.
Aafreen Sheikh 2012

문서로 내 원본 게시물을 편집했습니다. 성공적으로 Item 내부에 포함 된 배열로 모델을 사용하여 테스트 할 수 있었지만 불행히도 ItemTag가 자주 업데이트되므로 DBRef 여야합니다. 도움을 주셔서 다시 한 번 감사드립니다.
jschr

15

업데이트 : 댓글을 살펴보세요.이 답변은 질문과 정확하게 일치하지 않지만, (찬성 투표로 인해) 사용자의 다른 질문에 답변 할 수 있으므로이 "답변"을 삭제하지 않을 것입니다.

첫째 :이 질문이 정말 구식이라는 것을 알고 있지만 정확히이 문제를 검색했고이 SO 게시물은 Google 항목 # 1이었습니다. 그래서 docs.filter버전을 구현 했지만 (허용되는 답변) mongoose v4.6.0 문서를 읽었 으므로 이제 간단히 사용할 수 있습니다.

Item.find({}).populate({
    path: 'tags',
    match: { tagName: { $in: ['funny', 'politics'] }}
}).exec((err, items) => {
  console.log(items.tags) 
  // contains only tags where tagName is 'funny' or 'politics'
})

이것이 미래의 검색 기계 사용자에게 도움이되기를 바랍니다.


3
그러나 이것은 items.tags 배열 만 확실히 필터링 할 것입니까? tagName에 관계없이 항목이 반환됩니다 ...
OllyBarca

1
맞습니다, @OllyBarca. 문서에 따르면 일치는 인구 쿼리에만 영향을 미칩니다.
andreimarinescu

1
나는이 질문에 대답하지 않는 생각
Z.Alpha

1
@Fabian은 오류가 아닙니다. 모집단 쿼리 (이 경우 fans) 만 필터링됩니다. 리턴 실제 문서 (인 Story포함 fans영향되거나 필터링되지 않은 특성으로).
EnKrypt

2
따라서이 답변은 의견에 언급 된 이유 때문에 정확하지 않습니다. 앞으로 이것을 보는 사람은 조심해야합니다.
EnKrypt

3

최근에 같은 문제가 발생한 후 다음과 같은 해결책을 찾았습니다.

먼저 tagName이 'funny'또는 'politics'인 모든 ItemTag를 찾고 ItemTag _id 배열을 반환합니다.

그런 다음 태그 배열에서 모든 ItemTag _id를 포함하는 항목을 찾습니다.

ItemTag
  .find({ tagName : { $in : ['funny','politics'] } })
  .lean()
  .distinct('_id')
  .exec((err, itemTagIds) => {
     if (err) { console.error(err); }
     Item.find({ tag: { $all: itemTagIds} }, (err, items) => {
        console.log(items); // Items filtered by tagName
     });
  });

내가 한 방법 const tagsIds = await this.tagModel .find ({name : {$ in : tags}}) .lean () .distinct ( '_ id'); return this.adviceModel.find ({tags : {$ all : tagsIds}});
Dragos Lupei

1

@aaronheckmann의 대답은 나를 위해 일하지만 난 대체했다 return doc.tags.length;return doc.tags != null;해당 필드가 포함되어 있기 때문에 널 (null)을 이 채우기 내부에 기록 된 조건에 일치하지 않는 경우. 그래서 최종 코드 :

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags != null;
   })
   // do stuff with docs
});
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.