Zapytanie po wypełnieniu w Mongoose

83

Jestem całkiem nowy w Mongoose i MongoDB w ogóle, więc mam trudności z ustaleniem, czy coś takiego jest możliwe:

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

Czy jest lepszy sposób na zrobienie tego?

Edytować

Przepraszamy za zamieszanie. To, co próbuję zrobić, to zdobyć wszystkie przedmioty, które zawierają zabawny tag lub tag polityki.

Edytować

Dokument bez klauzuli gdzie:

[{ 
    _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 
 }, ... ]

Z klauzulą ​​where otrzymuję pustą tablicę.

jschr
źródło

Odpowiedzi:

61

Nowoczesna baza danych MongoDB w wersji wyższej niż 3.2 może służyć $lookupjako alternatywa .populate()w większości przypadków. Ma to również tę zaletę, że faktycznie wykonuje łączenie „na serwerze”, w przeciwieństwie do tego .populate(), co faktycznie polega na „wielokrotnym zapytaniu” w celu „emulacji” złączenia.

Więc .populate()to nie naprawdę „join” w sensie jak relacyjna baza danych to robi. Z $lookupdrugiej strony operator faktycznie wykonuje pracę na serwerze i jest mniej więcej analogiczny do „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"
  }
)

Uwaga : .collection.nametutaj w rzeczywistości jest wynikiem „ciągu”, czyli rzeczywistej nazwy kolekcji MongoDB przypisanej do modelu. Ponieważ mongoose domyślnie „ $lookuptworzy liczbę mnogą” nazw kolekcji i wymaga rzeczywistej nazwy kolekcji MongoDB jako argumentu (ponieważ jest to operacja na serwerze), jest to przydatna sztuczka do użycia w kodzie mangusty, w przeciwieństwie do „twardego kodowania” bezpośrednio nazwy kolekcji .

Chociaż moglibyśmy również użyć $filterw tablicach do usuwania niechcianych elementów, jest to w rzeczywistości najbardziej wydajna forma ze względu na optymalizację potoku agregacji dla specjalnego warunku, $lookuppo którym następuje zarówno warunek, jak $unwindi $matchwarunek.

W rzeczywistości powoduje to połączenie trzech etapów potoku w jeden:

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

Jest to wysoce optymalne, ponieważ faktyczna operacja „najpierw filtruje zbiór, aby dołączyć”, a następnie zwraca wyniki i „rozwija” tablicę. Obie metody są stosowane, więc wyniki nie przekraczają limitu BSON wynoszącego 16 MB, co jest ograniczeniem, którego klient nie ma.

Jedynym problemem jest to, że wydaje się to „sprzeczne z intuicją” pod pewnymi względami, szczególnie gdy chcesz uzyskać wyniki w tablicy, ale do tego właśnie $groupsłuży tutaj, ponieważ rekonstruuje do pierwotnej postaci dokumentu.

Szkoda również, że w tej chwili po prostu nie możemy pisać $lookupw tej samej ostatecznej składni, której używa serwer. IMHO, to jest przeoczenie, które należy naprawić. Ale na razie zwykłe użycie sekwencji zadziała i jest najbardziej realną opcją o najlepszej wydajności i skalowalności.

Dodatek - MongoDB 3.6 i nowsze

Chociaż pokazany tutaj wzorzec jest dość zoptymalizowany ze względu na to, jak inne etapy są wtaczane do $lookup, ma jeden błąd, ponieważ „LEFT JOIN”, który jest zwykle nieodłączny dla obu, $lookupa działania populate()jest negowane przez „optymalne” użycie $unwindtutaj, który nie zachowuje pustych tablic. Możesz dodać preserveNullAndEmptyArraysopcję, ale to neguje „zoptymalizowaną” sekwencję opisaną powyżej i zasadniczo pozostawia wszystkie trzy etapy nienaruszone, które normalnie byłyby połączone w optymalizacji.

MongoDB 3.6 rozszerza się o „bardziej wyrazistą” formę $lookupzezwalania na wyrażenie „pod-potok”. Co nie tylko spełnia cel polegający na zachowaniu „LEFT JOIN”, ale nadal umożliwia optymalne zapytanie w celu zmniejszenia zwracanych wyników i przy znacznie uproszczonej składni:

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

$exprUżywane w celu dopasowania deklarowana „lokalny” wartość z wartością „obcego” jest rzeczywiście to, co robi MongoDB „wewnętrznie” teraz z oryginalnego $lookupskładni. Wyrażając w tej formie możemy sami dostosować początkowe $matchwyrażenie w „pod-potoku”.

W rzeczywistości, jako prawdziwy „potok agregacji”, możesz zrobić prawie wszystko, co możesz zrobić z potokiem agregacji w ramach tego wyrażenia „potok podrzędny”, w tym „zagnieżdżać” poziomy w $lookupinnych powiązanych kolekcjach.

Dalsze użycie wykracza nieco poza zakres tego, o co chodzi w tym pytaniu, ale w odniesieniu do nawet „zagnieżdżonej populacji” nowy wzorzec użycia $lookuppozwala na to, aby było to prawie takie samo i „dużo” potężniejsze w pełnym wykorzystaniu.


Przykład roboczy

Poniżej przedstawiono przykład użycia metody statycznej w modelu. Po zaimplementowaniu tej statycznej metody wywołanie staje się po prostu:

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

Lub ulepszanie, aby być nieco bardziej nowoczesnym, staje się nawet:

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

.populate()Robi to bardzo podobnie do struktury, ale zamiast tego wykonuje łączenie na serwerze. Aby uzyskać kompletność, użycie tutaj rzutuje zwrócone dane z powrotem do instancji dokumentu mangusty zgodnie z przypadkami nadrzędnymi i podrzędnymi.

Jest dość trywialny i łatwy w adaptacji lub po prostu w użyciu, jak w większości typowych przypadków.

Uwaga : użycie async w tym miejscu służy jedynie zwięzłemu uruchomieniu załączonego przykładu. Rzeczywista implementacja jest wolna od tej zależności.

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

Lub trochę nowocześniejszy dla Node 8.x i nowszych async/awaitbez dodatkowych zależności:

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

A od MongoDB 3.6 i nowszych, nawet bez $unwindi $groupbudynku:

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

})()
Neil Lunn
źródło
3
Nie używam już Mongo / Mongoose, ale zaakceptowałem twoją odpowiedź, ponieważ jest to popularne pytanie i wygląda na to, że było pomocne dla innych. Cieszę się, że ten problem ma teraz bardziej skalowalne rozwiązanie. Dziękujemy za udzielenie zaktualizowanej odpowiedzi.
jschr
40

to, o co prosisz, nie jest bezpośrednio obsługiwane, ale można to osiągnąć, dodając kolejny krok filtru po zwróceniu zapytania.

Po pierwsze, .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )jest to zdecydowanie to, co musisz zrobić, aby filtrować dokumenty ze znacznikami. następnie po zwróceniu zapytania należy ręcznie odfiltrować dokumenty, które nie zawierają żadnych tagsdokumentów spełniających podane kryteria. coś jak:

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags.length;
   })
   // do stuff with docs
});
aaronheckmann
źródło
1
Hej Aaron, dzięki za odpowiedź. Może się mylę, ale czy $ in on populate () nie wypełni tylko dopasowanych tagów? Więc wszelkie dodatkowe tagi na elemencie zostaną odfiltrowane. Wygląda na to, że będę musiał wypełnić wszystkie elementy i wtedy drugi etap filtrowania zredukuje go na podstawie nazwy tagu.
jschr
@aaronheckmann Zaimplementowałem Twoje sugerowane rozwiązanie, masz rację, aby wykonać filtrowanie po .exec, ponieważ chociaż zapytanie wypełniające wypełnia tylko wymagane obiekty, ale nadal zwraca cały zestaw danych. Czy uważasz, że w nowszej wersji Mongoose jest jakaś opcja zwracania tylko wypełnionego zestawu danych, więc nie musimy przechodzić do kolejnego filtrowania?
Aqib Mumtaz
Ciekawi mnie również wydajność. Jeśli zapytanie zwraca na końcu cały zestaw danych, to nie ma sensu filtrowanie populacji? Co mówisz? Dostosowuję zapytanie populacyjne do optymalizacji wydajności, ale w ten sposób wydajność nie poprawi się w przypadku dużego zbioru danych?
Aqib Mumtaz
mongoosejs.com/docs/api.html#query_Query-populate ma wszystkie szczegóły, jeśli ktoś jest zainteresowany
samazi
jak dopasować w różnych polach po wypełnieniu?
nicogaldo
20

Spróbuj wymienić

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

przez

.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )
Aafreen Sheikh
źródło
1
Dziękuję za odpowiedź. Uważam, że to tylko zapełnia każdą pozycję zabawnymi lub politycznymi informacjami, co nie zmniejszyłoby listy rodziców. Tak naprawdę chciałbym tylko te przedmioty, które mają zabawny lub polityczny tag w etykiecie.
jschr
Czy możesz pokazać, jak wygląda Twój dokument? Bo „gdzie” w tablicy tagów wydaje mi się prawidłową operacją… Czy po prostu otrzymaliśmy błędną składnię… Czy próbowałeś całkowicie usunąć tę klauzulę „gdzie” i sprawdzić, czy coś jest zwracane? Alternatywnie, aby sprawdzić, czy napisanie „tags.tagName” jest poprawne składniowo, możesz na chwilę zapomnieć o ref i wypróbować zapytanie z osadzoną tablicą w dokumencie „Item”.
Aafreen Sheikh
Edytowałem mój oryginalny post z dokumentem. Udało mi się przetestować to z modelem jako osadzoną tablicą wewnątrz elementu z powodzeniem, ale niestety wymagam, aby był to DBRef, ponieważ ItemTag jest często aktualizowany. Jeszcze raz dziękuję za pomoc.
jschr
15

Aktualizacja: Proszę spojrzeć na komentarze - ta odpowiedź nie pasuje poprawnie do pytania, ale może odpowiada na inne pytania użytkowników, z którymi się zetknęli (myślę, że ze względu na upvotes), więc nie będę usuwał tej „odpowiedzi”:

Po pierwsze: wiem, że to pytanie jest naprawdę nieaktualne, ale szukałem dokładnie tego problemu, a ten post SO to wpis Google nr 1. Więc zaimplementowałem docs.filterwersję (zaakceptowana odpowiedź), ale jak przeczytałem w dokumentacji mongoose v4.6.0 , możemy teraz po prostu użyć:

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

Mam nadzieję, że pomoże to przyszłym użytkownikom wyszukiwarek.

fabiański
źródło
3
Ale to na pewno odfiltruje tylko tablicę items.tags? Przedmioty zostaną zwrócone bez względu na tagName ...
OllyBarca
1
Zgadza się, @OllyBarca. Zgodnie z dokumentacją dopasowanie wpływa tylko na zapytanie dotyczące populacji.
andreimarinescu
1
Myślę, że to nie odpowiada na pytanie
Z.Alpha
1
@Fabian to nie jest błąd. Tylko zapytanie populacyjne (w tym przypadku fans) jest filtrowane. Na rzeczywisty zwrócony dokument (który Storyzawiera fansjako właściwość) nie ma to wpływu ani nie jest filtrowany.
EnKrypt
2
Ta odpowiedź jest zatem nieprawidłowa z powodów wymienionych w komentarzach. Każdy, kto patrzy na to w przyszłości, powinien zachować ostrożność.
EnKrypt
3

Po tym, jak sam miałem ostatnio ten sam problem, wymyśliłem następujące rozwiązanie:

Najpierw znajdź wszystkie ItemTag, w których tagName to „śmieszne” lub „polityka”, i zwróć tablicę z identyfikatorami ItemTag _ids.

Następnie znajdź elementy, które zawierają wszystkie identyfikatory ItemTag _ids w tablicy tags

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
     });
  });
OllyBarca
źródło
Jak to zrobiłem const tagsIds = await this.tagModel .find ({name: {$ in: tags}}) .lean () .distinct ('_ id'); return this.adviceModel.find ({tags: {$ all: tagsIds}});
Dragos Lupei
1

Odpowiedź @aaronheckmann zadziałała dla mnie, ale musiałem zamienić return doc.tags.length;na, return doc.tags != null;ponieważ to pole zawiera null, jeśli nie pasuje do warunków zapisanych w wypełnieniu. A więc ostateczny kod:

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags != null;
   })
   // do stuff with docs
});
HernanFila
źródło