Truy vấn sau khi điền trong Mongoose


81

Tôi còn khá mới đối với Mongoose và MongoDB nói chung nên tôi đang gặp khó khăn trong việc tìm hiểu xem liệu điều này có khả thi không:

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

Có cách nào tốt hơn để làm điều này không?

Biên tập

Xin lỗi vì bất kỳ sự nhầm lẫn nào. Những gì tôi đang cố gắng làm là lấy tất cả các Mục có chứa thẻ hài hước hoặc thẻ chính trị.

Biên tập

Tài liệu không có mệnh đề 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 
 }, ... ]

Với mệnh đề where, tôi nhận được một mảng trống.

Câu trả lời:


59

Với MongoDB hiện đại lớn hơn 3.2, bạn có thể sử dụng $lookupthay thế .populate()trong hầu hết các trường hợp. Điều này cũng có lợi thế là thực sự thực hiện tham gia "trên máy chủ" trái ngược với những gì .populate()thực sự là "nhiều truy vấn" để "mô phỏng" một tham gia.

Vì vậy, .populate()không thực sự là một "tham gia" theo nghĩa như thế nào một cơ sở dữ liệu quan hệ hiện nó. Mặt khác, $lookuptoán tử thực sự thực hiện công việc trên máy chủ và ít nhiều tương tự với "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đây thực sự đánh giá "chuỗi" là tên thực của bộ sưu tập MongoDB như được gán cho mô hình. Vì mongoose "đa nguyên hóa" tên bộ sưu tập theo mặc định và$lookup cần tên bộ sưu tập MongoDB thực tế làm đối số (vì đó là hoạt động của máy chủ), nên đây là một thủ thuật hữu ích để sử dụng trong mã mongoose, trái ngược với "mã hóa cứng" tên bộ sưu tập trực tiếp .

Mặc dù chúng tôi cũng có thể sử dụng $filtertrên các mảng để loại bỏ các mục không mong muốn, nhưng đây thực sự là biểu mẫu hiệu quả nhất do Tối ưu hóa đường ống tổng hợp cho điều kiện đặc biệt $lookuptheo sau là cả điều kiện $unwind$matchđiều kiện.

Điều này thực sự dẫn đến ba giai đoạn đường ống được cuộn thành một:

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

Điều này rất tối ưu vì hoạt động thực tế "lọc bộ sưu tập để tham gia trước", sau đó nó trả về kết quả và "giải nén" mảng. Cả hai phương pháp đều được sử dụng để kết quả không phá vỡ giới hạn BSON 16MB, đây là một hạn chế mà máy khách không có.

Vấn đề duy nhất là nó có vẻ "phản trực quan" theo một số cách, đặc biệt khi bạn muốn kết quả trong một mảng, nhưng đó là những gì $groupở đây, vì nó cấu trúc lại dạng tài liệu ban đầu.

Cũng thật không may là tại thời điểm này, chúng ta không thể thực sự viết $lookuptheo cùng một cú pháp cuối cùng mà máy chủ sử dụng. IMHO, đây là một sơ suất cần được sửa chữa. Nhưng hiện tại, chỉ cần sử dụng trình tự sẽ hoạt động và là lựa chọn khả thi nhất với hiệu suất và khả năng mở rộng tốt nhất.

Phụ lục - MongoDB 3.6 trở lên

Mặc dù mô hình hiển thị ở đây khá tối ưu hóa do cách các giai đoạn khác được đưa vào $lookup, nhưng nó có một điểm không thành công là "LEFT JOIN" vốn thường có sẵn cho cả hai $lookupvà các hành động của populate()bị phủ nhận bởi cách sử dụng "tối ưu"$unwind ở đây không bảo tồn các mảng trống. Bạn có thể thêm preserveNullAndEmptyArraystùy chọn, nhưng điều này phủ nhận trình tự "được tối ưu hóa" được mô tả ở trên và về cơ bản giữ nguyên cả ba giai đoạn thường được kết hợp trong tối ưu hóa.

MongoDB 3,6 nở với một "biểu cảm hơn" hình thức $lookupcho phép một "tiểu đường ống" biểu. Điều này không chỉ đáp ứng mục tiêu giữ lại "LEFT JOIN" mà vẫn cho phép truy vấn tối ưu để giảm kết quả trả về và với cú pháp đơn giản hơn nhiều:

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

Giá trị $exprđược sử dụng để khớp giá trị "cục bộ" đã khai báo với giá trị "nước ngoài" thực sự là những gì MongoDB thực hiện "nội bộ" bây giờ với $lookupcú pháp ban đầu . Bằng cách diễn đạt ở dạng này, chúng ta có thể tự điều chỉnh $matchbiểu thức ban đầu trong "đường ống con".

Trên thực tế, với tư cách là một "đường ống tổng hợp" thực sự, bạn có thể làm bất cứ điều gì bạn có thể làm với đường ống tổng hợp trong biểu thức "đường ống con" này, bao gồm cả việc "lồng" các cấp độ của $lookup các tập hợp liên quan khác.

Việc sử dụng thêm vượt quá phạm vi của những gì câu hỏi ở đây yêu cầu một chút, nhưng liên quan đến thậm chí "quần thể lồng nhau" thì kiểu sử dụng mới của $lookupcho phép điều này giống nhau và mạnh hơn "rất nhiều" trong việc sử dụng đầy đủ.


Ví dụ làm việc

Sau đây là một ví dụ sử dụng phương thức tĩnh trên mô hình. Khi phương thức tĩnh đó được triển khai, cuộc gọi chỉ đơn giản trở thành:

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

Hoặc nâng cao để hiện đại hơn một chút thậm chí trở thành:

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

Làm cho nó rất giống với .populate() cấu trúc, nhưng thay vào đó, nó thực sự thực hiện tham gia trên máy chủ. Để hoàn chỉnh, cách sử dụng ở đây sẽ chuyển dữ liệu được trả về trở lại các cá thể tài liệu mongoose theo cả trường hợp mẹ và con.

Nó khá nhỏ và dễ thích ứng hoặc chỉ sử dụng như đối với hầu hết các trường hợp phổ biến.

NB Việc sử dụng async ở đây chỉ để nói ngắn gọn là chạy ví dụ kèm theo. Việc triển khai thực tế không có sự phụ thuộc này.

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

Hoặc hiện đại hơn một chút cho Node 8.x trở lên với async/awaitvà không có phụ thuộc bổ sung:

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

Và từ MongoDB 3.6 trở lên, ngay cả khi không có $unwind$groupxây dựng:

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
Tôi không còn sử dụng Mongo / Mongoose nữa nhưng tôi đã chấp nhận câu trả lời của bạn vì đây là một câu hỏi phổ biến và có vẻ như điều này rất hữu ích cho những người khác. Rất vui khi thấy vấn đề này hiện đã có một giải pháp mở rộng hơn. Cảm ơn bạn đã cung cấp câu trả lời cập nhật.
jschr

40

những gì bạn đang yêu cầu không được hỗ trợ trực tiếp nhưng có thể đạt được bằng cách thêm một bước lọc khác sau khi truy vấn trả về.

đầu tiên, .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )chắc chắn là những gì bạn cần làm để lọc tài liệu thẻ. sau đó, sau khi truy vấn trả về, bạn sẽ cần lọc ra theo cách thủ công các tài liệu không có bất kỳ tagstài liệu nào phù hợp với tiêu chí điền. cái gì đó như:

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

1
Này Aaron, cảm ơn vì đã trả lời. Tôi có thể sai nhưng $ in trên populate () sẽ không chỉ điền các thẻ phù hợp? Vì vậy, bất kỳ thẻ bổ sung nào trên mục sẽ được lọc ra. Có vẻ như tôi sẽ phải điền tất cả các mục và có bước lọc thứ hai để giảm nó dựa trên tên thẻ sau đó.
jschr

@aaronheckmann Tôi đã triển khai giải pháp được đề xuất của bạn, bạn chuẩn bị thực hiện bộ lọc sau .exec, bởi vì mặc dù truy vấn điền thông tin chỉ điền các đối tượng được yêu cầu nhưng vẫn trả về toàn bộ tập dữ liệu của nó. Bạn có nghĩ rằng trong phiên bản Mongoose mới hơn, có một số tùy chọn để chỉ trả lại tập dữ liệu đã phổ biến để chúng tôi không cần thực hiện một bộ lọc khác không?
Aqib Mumtaz

Tôi cũng tò mò muốn biết về hiệu suất, Nếu truy vấn trả về toàn bộ tập dữ liệu ở cuối thì không có mục đích đi lọc dân số? bạn nói gì? Tôi đang điều chỉnh truy vấn dân số để tối ưu hóa hiệu suất nhưng theo cách này hiệu suất sẽ không tốt hơn cho tập dữ liệu lớn?
Aqib Mumtaz

mongoosejs.com/docs/api.html#query_Query-populate có tất cả các chi tiết nếu bất cứ ai khác quan tâm
samazi

làm thế nào khớp trong các trường khác nhau khi được điền?
nicogaldo

19

Thử thay thế

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

bởi

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

1
Cảm ơn vi đa trả lơi. Tôi tin rằng những gì điều này làm là chỉ điền vào mỗi mục với hài hước hoặc chính trị, điều này sẽ không làm giảm danh sách mẹ. Những gì tôi thực sự muốn chỉ là các mục có nội dung hài hước hoặc chính trị trong thẻ của họ.
jschr

Bạn có thể cho biết tài liệu của bạn trông như thế nào không? Coz một 'where' bên trong mảng thẻ có vẻ như là một hoạt động hợp lệ đối với tôi..Chúng tôi chỉ sai cú pháp thôi..Bạn đã thử xóa hoàn toàn mệnh đề 'where' đó và kiểm tra xem có gì được trả lại không? Ngoài ra, chỉ để kiểm tra xem việc viết 'tags.tagName' có ổn về mặt cú pháp hay không, bạn có thể quên điều ref trong một thời gian và thử truy vấn của mình với một mảng được nhúng bên trong tài liệu 'Item'.
Aafreen Sheikh

Đã chỉnh sửa bài viết gốc của tôi với tài liệu. Tôi đã có thể kiểm tra nó với mô hình dưới dạng một mảng được nhúng bên trong Item thành công nhưng rất tiếc, tôi yêu cầu nó phải là DBRef vì ItemTag thường xuyên được cập nhật. Cảm ơn một lần nữa vì sự giúp đỡ.
jschr

15

Cập nhật: Vui lòng xem các nhận xét - câu trả lời này không khớp chính xác với câu hỏi, nhưng có thể nó trả lời các câu hỏi khác của người dùng đã gặp phải (tôi nghĩ rằng do lượt ủng hộ) nên tôi sẽ không xóa "câu trả lời" này:

Đầu tiên: Tôi biết câu hỏi này đã thực sự lỗi thời, nhưng tôi đã tìm kiếm chính xác vấn đề này và bài đăng SO này là mục nhập số 1 của Google. Vì vậy, tôi đã triển khai docs.filterphiên bản (câu trả lời được chấp nhận) nhưng khi tôi đọc trong tài liệu mongoose v4.6.0, bây giờ chúng ta có thể chỉ cần sử dụng:

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

Hy vọng điều này sẽ giúp người dùng máy tìm kiếm trong tương lai.


3
Nhưng điều này sẽ chỉ lọc mảng items.tags chắc chắn? Các mặt hàng sẽ được trả lại bất kể tagName ...
OllyBarca

1
Đó là chính xác, @OllyBarca. Theo tài liệu, đối sánh chỉ ảnh hưởng đến truy vấn tập hợp.
andreimarinescu

1
Tôi nghĩ rằng điều này không trả lời câu hỏi
Z.Alpha

1
@Fabian đó không phải là một lỗi. Chỉ truy vấn dân số (trong trường hợp này fans) được lọc. Tài liệu thực tế được trả về (được Storychứa fansdưới dạng thuộc tính) không bị ảnh hưởng hoặc bị lọc.
EnKrypt

2
Do đó, câu trả lời này không đúng, vì những lý do được đề cập trong các bình luận. Bất cứ ai nhìn vào điều này trong tương lai nên cẩn thận.
EnKrypt

3

Sau khi bản thân gặp vấn đề tương tự gần đây, tôi đã đưa ra giải pháp sau:

Trước tiên, hãy tìm tất cả các ItemTags trong đó tagName là 'vui nhộn' hoặc 'chính trị' và trả về một mảng ItemTag _ids.

Sau đó, tìm các Mục chứa tất cả các _id ItemTag trong mảng thẻ

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

Tôi đã thực hiện như thế nào const tagsIds = await this.tagModel .find ({name: {$ in: tags}}) .lean () .distinction ('_ id'); return this.adviceModel.find ({tags: {$ all: tagsIds}});
Dragos Lupei

1

Câu trả lời của @aaronheckmann phù hợp với tôi nhưng tôi phải thay thế return doc.tags.length;thành return doc.tags != null;vì trường đó chứa null nếu nó không khớp với các điều kiện được viết bên trong điền. Vì vậy, mã cuối cùng:

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags != null;
   })
   // do stuff with docs
});
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.