UFO ET IT

Mongoose에서 채우기 후 쿼리

ufoet 2020. 11. 21. 08:36
반응형

Mongoose에서 채우기 후 쿼리


나는 일반적으로 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 절을 사용하면 빈 배열이 생성됩니다.


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()
  }

})()

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

first, .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } ) is definitely what you need to do to filter the tags documents. then, after the query returns you'll need to manually filter out documents that don't have any tags docs that matched the populate criteria. something like:

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

Try replacing

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

by

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

Update: Please take a look at the comments - this answer does not correctly match to the question, but maybe it answers other questions of users which came across (I think that because of the upvotes) so I will not delete this "answer":

First: I know this question is really outdated, but I searched for exactly this problem and this SO post was the Google entry #1. So I implemented the docs.filter version (accepted answer) but as I read in the mongoose v4.6.0 docs we can now simply use:

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'
})

Hope this helps future search machine users.


After having the same problem myself recently, I've come up with the following solution:

First, find all ItemTags where tagName is either 'funny' or 'politics' and return an array of ItemTag _ids.

Then, find Items which contain all ItemTag _ids in the tags array

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
     });
  });

@aaronheckmann 's answer worked for me but I had to replace return doc.tags.length; to return doc.tags != null; because that field contain null if it doesn't match with the conditions written inside populate. So the final code:

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

참고URL : https://stackoverflow.com/questions/11303294/querying-after-populate-in-mongoose

반응형