back to top

MongoDB: interrogare i database e recuperare dati specifici

In questa lezione vedremo come usare i metodi db.collection.findOne() e db.collection.find() per estrarre dei documenti in base a criteri di selezione più o meno complessi.

Il metodo db.collection.findOne()

Il metodo db.collection.findOne(query, projection) restituisce un solo documento della collezione che soddisfa i criteri del documento passato come primo argomento. Possiamo specificare una o più condizioni aggiungendo più coppie campo/valore al documento. Nel caso in cui siano presenti diversi campi, verrà eseguito un AND logico e restituito un solo documento che soddisfi tutte le condizioni.

Vediamo subito un esempio utilizzando i database e le collezioni di esempio che nelle lezioni precedenti abbiamo caricato su MongoDB Atlas. In particolare, selezioniamo il database ‘sample_restaurants’ (use sample_restaurants) e cerchiamo un ristorante italiano a Brooklyn situato in un edificio il cui ‘zipcode’ è ‘11229’.

db.restaurants.findOne(
  {
    "borough": "Brooklyn", 
    "cuisine": "Italian", 
    "address.zipcode": "11229"
  }
)

In questo caso passiamo come primo argomento un documento con tre campi. Verrà restituito il primo documento della collezione ‘restaurants’ che soddisfa contemporaneamente (viene implicitamente eseguito l’operatore logico AND) i tre criteri.

Se volessimo anche indicare quali campi devono essere inclusi o meno nel risultato finale, potremmo passare un secondo argomento ‘projection’.

Dovremmo semplicemente passare un documento con i campi da includere o rimuovere. Per includere un campo nel risultato finale assegniamo il valore 1, al contrario assegniamo 0 per escluderlo. Il campo ‘_id’ sarà sempre incluso a meno che non lo escludiamo in modo esplicito.

Torniamo al nostro esempio ed eseguiamo la stessa query, ma passiamo un argomento per includere nel documento finale solo i campi: ‘borough’, ‘cuisine’, ‘name’.

db.restaurants.findOne(
  {
    "borough": "Brooklyn", 
    "cuisine": "Italian", 
    "address.zipcode": "11229"
  }, 
  {
    "borough": 1, 
    "cuisine": 1, 
    "name": 1, 
    "_id": 0
  }
)

{ borough: 'Brooklyn',
  cuisine: 'Italian',
  name: 'Michael\'S Restaurant' 
}

Il documento db.collection.findOne() ritorna un solo documento. Può essere utile in alcuni casi e si utilizza spesso per capire quali sono i campi dei documenti di una collezione. In quest’ultimo caso eseguiamo il metodo senza alcun argomento db.restaurants.findOne() ed otteniamo un documento della collezione.

Un limite di questo metodo è che otteniamo un risultato parziale nel caso in cui siano presenti più documenti che soddisfino i criteri di selezione.

Per ottenere tutti i documenti useremo invece il metodo db.collection.find().

Interrogare un database con db.collection.find()

Il metodo db.collection.find(query, projection) ha la stessa segnatura di findOne(query, projection), ma consente di ottenere un cursore, ovvero un puntatore all’insieme di documenti restituiti dalla query. Un cursore presenta inoltre diversi metodi utili.

Possiamo ripetere la query eseguita in precedenza utilizzando però find() ed otteniamo come risultato tutti i documenti che soddisfano i criteri di selezione.

db.restaurants.find(
  {
    "borough": "Brooklyn", 
    "cuisine": "Italian", 
    "address.zipcode": "11229"
  }, 
  {
    "borough": 1, 
    "cuisine": 1, 
    "name": 1, 
    "_id": 0
  }
)

{ borough: 'Brooklyn',
  cuisine: 'Italian',
  name: 'Michael\'S Restaurant' 
}
{ borough: 'Brooklyn', 
  cuisine: 'Italian',
  name: 'J-M Pizza Ii' 
}
{ borough: 'Brooklyn', 
  cuisine: 'Italian',
  name: 'La Trattoria'
}

Dal momento che find() restituisce un cursore, possiamo eseguire altri metodi come count() che conta il numero di documenti referenziati dal cursore. Possiamo appendere count() direttamente al metodo find().

db.restaurants.find(
  {
    "borough": "Brooklyn", 
    "cuisine": "Italian", 
    "address.zipcode": "11229"
  }, 
  {
    "borough": 1, 
    "cuisine": 1, 
    "name": 1, 
    "_id": 0
  }
).count()

3

Un altro metodo che può tornare utile è cursor.pretty() che configura il cursore in modo da visualizzare i risultati in un formato semplice da leggere, nel caso in cui i documenti restituiti non siano formattati correttamente.

Operatori di confronto

MQL (MongoDB Query Language) comprende vari operatori di confronto che possiamo usare nelle query. Come quelli già incontrati nelle precedenti lezioni, anche il nome di questi operatori inizia col carattere ‘$’. In MQL il carattere ‘$’ è utilizzato per varie funzionalità, una di queste è proprio contraddistinguere gli operatori.

Gli operatori di confronto presentano la stessa sintassi:

{<field1>: {<operator>: <value>}}

Fra gli operatori di confronto abbiamo:

  • $eq serve per specificare una condizione di uguaglianza. L’operatore $eq è equivalente alla sintassi usata nei precedenti esempi {campo: <valore>}. Per esempio db.restaurants.find({"borough": "Brooklyn"}) è equivalente a db.restaurants.find({borough: {$eq: "Brooklyn"}}).
  • $ne è l’opposto dell’operatore $eq.
  • $gt e $gte selezionano i documenti in cui il valore del campo è rispettivamente maggiore ($gt) e maggiore o uguale ($gte) al valore specificato.
  • $lt e $lte selezionano i documenti in cui il valore del campo è rispettivamente minore ($lt) e minore o uguale ($lte) al valore specificato.

Facendo riferimento sempre ai database caricati nelle precedenti lezioni su MongoDB Atlas, possiamo interrogare il database ‘sample_mflix’ e cercare nella collezione ‘movies’ i film rilasciati fra il 1998 e 1999 il cui titolo inizia con ‘The’.

db.movies.find(
  {
    title: /^The/,
    year: {$gt: 1998, $lte: 1999}
  }, 
  {title: 1, year: 1, _id: 0}
)

Come potete notare, abbiamo utilizzato gli operatori $gt e $lte per filtrare i film in base all’anno di rilascio. Inoltre abbiamo usato un’espressione regolare per scegliere solo i film il cui titolo inizia con ‘The’. Per quanto riguarda il campo ‘year’, anche se esiste un operatore $and, elencare le condizioni in un singolo documento {$gt: 1998, $lte: 1999} è equivalente ad un AND logico.

Valori null e $exists

L’operatore $exists è di tipo booleano e presenta la seguente sintassi:

{ campo: { $exists: <boolean> } }

Se $exists è pari a true, vengono restituiti i documenti che contengono il campo specificato anche se questo è pari a null. In caso contrario otteniamo solo i documenti in cui il campo non esiste.

Vediamo un esempio e creiamo prima una collezione ‘users’ come mostrato sotto.

> var users = [
  {
    name: 'Taylor Hickle',
    email: '[email protected]',
    city: 'Milano',
    age: 44,
    pro: true
  },
  {
    name: 'Clara Luettgen',
    email: '[email protected]',
    city: 'Bologna',
    age: 35,
    pro: false
  },
  {
    name: 'Mr. Philip Kunde',
    email: '[email protected]',
    city: 'Lecce',
    age: 26,
    pro: null
  },
  {
    name: 'Bobby Spinka',
    email: '[email protected]',
    city: 'Cagliari',
    age: 52
  }
]

> db.users.insertMany(users)

Se usiamo $exists per cercare i documenti che presentano un campo ‘pro’, otteniamo i primi 3 documenti compreso quello in cui il valore è pari a null.

> db.users.find({pro: {$exists: true}})

Per quanto riguarda i campi con valore null, bisogna prestare attenzione perché se cerchiamo documenti con un campo pari a null, verranno restituiti anche quelli in cui quel campo è mancante. Se per esempio eseguiamo la seguente query db.users.find({pro: null}) otteniamo i due documenti relativi agli utenti ‘Mr. Philip Kunde’ e ‘Bobby Spinka’.

Se vogliamo solo il documento in cui ‘pro’ è pari a null, dovremo lanciare la query sottostante.

> db.users.find({pro: {$eq: null, $exists: true}})

Per ottenere invece i documenti in cui esiste un campo ‘pro’ che non è pari a null eseguiamo invece:

> db.users.find({pro: {$ne: null, $exists: true}})

Operatori logici: $not, $and, $or

Fra gli operatori logici presenti in MQL, $not esegue l’operazione logica NOT sull’espressione o risultato dell’operazione passata come argomento.

La sintassi è la seguente:

{ field: { $not: { <operator-expression> } } }

Per esempio, riprendiamo la query vista in precedenza per selezionare i film rilasciati nel ’98 e ’99. In questo caso cerchiamo i film di quegli anni il cui titolo NON inizia con ‘The’.

> db.movies.find(
  {
    title: {$not: /^The/},
    year: {$gt: 1998, $lte: 1999}
  }, 
  {
    title: 1,
    year: 1,
    _id: 0
  }
)

Abbiamo già avuto modo di vedere che MongoDB esegue implicitamente l’AND logico fra i campi del documento passato come primo argomento dei metodi findOne() e find()

Ma esiste anche un operatore $and per eseguire l’operazione di AND logico esplicitamente. Può risultare utile nel caso di query complesse o in combinazioni di altri operatori logici.

{ $and: [ { <expression1> }, { <expression2> } , ... , { <expressionN> } ] }

Per esempio possiamo cercare tutti i ristoranti irlandesi nel Bronx con una delle due seguenti query equivalenti.

> db.restaurants.find(
  {
    $and: [
      {borough: 'Bronx'},
      {cuisine: 'Irish'}
    ]
  }, 
  {
    name: 1, 
    borough: 1, 
    cuisine: 1, 
    _id: 0
  }
)
> db.restaurants.find(
  {borough: 'Bronx', cuisine: 'Irish'}, 
  {name: 1, borough: 1, cuisine: 1, _id: 0}
)

Esiste anche un operatore $or che può essere utilizzato per eseguire l’operazione di OR logico su uno o più campi.

La sintassi è simile a quella dell’operatore $and.

{ $or: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] }

Per esempio vediamo come cercare tutti i ristoranti di Brooklyn indipendentemente dal tipo di cucina oppure i ristoranti italiani presenti in qualsiasi quartiere.

> db.restaurants.find(
  {
    $or: [
      {borough: 'Brooklyn'},
      {cuisine: 'Italian'}
    ]
  }
)

Esiste un altro operatore che può essere usato per eseguire l’OR logico sui valori di un singolo campo. Stiamo parlando di $in che consente di scegliere i documenti in cui il valore di un campo è uguale a uno dei valori dell’array assegnato a $in.

Per esempio possiamo ottenere i documenti relativi ai ristoranti di cucina italiana o americana come mostrato sotto.

> db.restaurants.find(
  {
    cuisine: {
      $in: ['Italian', 'American']
    }
  }
)

Oltre a $in esiste un altro operatore $nin che è l’opposto di $in. $nin seleziona i documenti in cui il valore di un campo non è nell’array assegnato a $nin oppure il campo non esiste proprio.

Cerchiamo allora i documenti relativi ai ristoranti che non offrono piatti italiani, americani o irlandesi.

> db.restaurants.find(
  {
    cuisine: {
      $nin: ['Italian', 'American', 'Irish']
    }
  }
)

Ovviamente possiamo combinare più operatori insieme. Per esempio possiamo cercare i ristoranti di cucina italiana o cinese a Brooklyn.

db.restaurants.find(
  { 
    $and: [
      {$or: [
        {cuisine: 'Italian'}, 
        {cuisine: 'Chinese'}
      ]}, 
      {borough: 'Brooklyn'}
    ]
  }
)

Ma possiamo semplificare la query usando l’operatore $in.

db.restaurants.find(
  {
    cuisine: {
      $in: ['Italian', 'Chinese']
    }, 
    borough: 'Brooklyn'
  }
)

L’operatore $expr

L’operatore $expr consente di usare gli operatori dell’Aggregation Framework all’interno di una query per poter eseguire così delle interrogazioni più complesse. Parleremo dell’Aggregation Framework in una delle prossime lezioni.

Ora vediamo invece come usare $expr con il metodo db.collection.find() in un caso particolare, ovvero quando vogliamo confrontare due campi all’interno dello stesso documento.

Prendiamo in considerazione ancora una volta i database caricati su Atlas ed in particolare la collezione ‘listingsAndReviews’ di ‘sample_airbnb’.

Supponiamo di voler cercare le proprietà in Spagna per le quali la tariffa per le pulizie (campo ‘cleaning_fee’) sia presente nel documento, sia diversa da 0 ma inferiore al 10% del prezzo. Vogliamo inoltre visualizzare i soli campi ‘cleaning_fee’, ‘price’, ‘property_type’ e ‘address.street’.

Possiamo usare l’operatore $expr per confrontare i valori dei due campi ‘price’ e ‘cleaning_fee’ di un documento.

> db.listingsAndReviews.find(
  {
    cleaning_fee: {$exists: true, $ne: 0}, 
    "address.country": "Spain" , 
    $expr: { $lt: ["$cleaning_fee", { $multiply: ["$price", 0.1] }]}
  },
  {
    _id: 0, 
    cleaning_fee: 1, 
    price: 1, 
    property_type: 1, 
    "address.street": 1
  }
)

L’unica novità presente nell’ultima query è proprio l’operatore $expr.

Soffermiamoci sull’operatore $expr partendo dalla sua sintassi.

{ $expr: { <expression> } }

L’espressione che passiamo a $expr nell’esempio comprende l’operatore $lt in una forma diversa da quella vista in precedenza. Si tratta della sintassi dell’operatore $lt dell’Aggregation Framework che compara due valori e restituisce true se il primo valore è minore del secondo e false in caso contrario.

{ $lt: [ <expression1>, <expression2> ] }

Il primo elemento dell’array assegnato all’operatore $lt presenta una peculiarità. Il nome del campo ‘cleaning_fee’ è infatti preceduto dal carattere ‘$’ ("$cleaning_fee"). Scopriamo ora un altro uso del carattere ‘$’ che oltre ad identificare degli operatori, viene in questo caso usato per indicare che ci stiamo riferendo proprio al valore del campo ‘cleaning_fee’ e non al campo stesso. Lo stesso vale per ‘$price’.

In pratica stiamo chiedendo di confrontare il valore del campo ‘cleaning_fee’ di un documento con il risultato della moltiplicazione fra il valore del campo ‘price’ dello stesso documento ed il fattore 0.1.

Un documento viene incluso nel risultato finale solo se il valore del campo ‘cleaning_fee’ è inferiore al 10% del valore del campo ‘price’.

Anche $multiply è un operatore dell’Aggregation Framework che moltiplica gli elementi dell’array assegnato e restituisce il risultato.

db.collection.find() e campi di tipo Array e documenti annidati

Abbiamo visto finora come specificare delle condizioni di uguaglianza per campi semplici.

Per quanto riguarda gli array dovremo prestare un po’ di attenzione.

Consideriamo la collezione ‘posts’ del database ‘sample_training’ che abbiamo importato in MongoDB Atlas. Ciascun documento presenta un campo ‘tags’ di tipo array. Se eseguiamo la seguente query, saranno restituiti i documenti che hanno un campo ‘tags’ esattamente uguale all’array riportato sotto, compreso l’ordine in cui sono riportate le etichette dell’array.

> tags = [ 
  'watchmaker',
  'santa', 
  'xylophone', 
  'math',
  'handsaw', 
  'dream', 
  'undershirt', 
  'dolphin',
  'tanker',
  'action'
]

> db.posts.find({ tags })

Invece, se vogliamo cercare un documento con un campo di tipo array che contiene tutti gli elementi specificati, indipendentemente dall’ordine o da altri elementi in esso contenuti, possiamo usare l’operatore $all.

{ <field>: { $all: [ <value1> , <value2> ... ] } }

Vediamo allora l’operatore $all in azione usando la collezione listingsAndReviews del database sample_airbnb.

La seguente query restituisce i documenti con un campo servizi (amenities) che comprende tutti e tre gli elementi dell’array ‘requirements’ in qualsiasi ordine.

> var requirements = ['Wifi', 'Hot water', 'Washer']
> db.listingsAndReviews.find({amenities: {$all: requirements}})

Se invece vogliamo i documenti il cui campo ‘amenities’ comprenda almeno 1 degli elementi dell’array ‘requirements’, ma non per forza tutti e 3 gli elementi, possiamo usare un operatore già incontrato in precedenza, ovvero $in.

> var requirements = ['Wifi', 'Hot water', 'Washer']
> db.listingsAndReviews.find({amenities: {$in: requirements}})

L’operatore $size restituisce invece i documenti in cui il campo di tipo array su cui viene applicato ha un numero di elementi pari al valore assegnato a $size (sintassi: { field: { $size: <size> } } ).

> var requirements = ['Wifi', 'Hot water', 'Washer']
> db.listingsAndReviews.find({amenities: {$all: requirements, $size: 13}})

La precedente query restituisce i documenti il cui campo ‘amenities’ di tipo array comprende tutti e 3 gli elementi dell’array ‘requirements’, inoltre il campo ‘amenities’ deve contenere esattamente 13 servizi.

Un altro operatore utile è $elemMatch che permette di ottenere i documenti che contengono un campo array in cui almeno un elemento soddisfa la condizione assegnata.

La sintassi è quindi:

{ <field>: { $elemMatch: { <query1>, <query2>, ... } } }

Vediamo un esempio supponendo di creare una collezione con i seguenti documenti.

> var students = [
  {
    studend_id: 0,
    scores: [23, 26, 27]
  },
  {
    studend_id: 1,
    scores: [18, 22, 21]
  },
  {
    studend_id: 2,
    scores: [26, 28, 30]
  },
  {
    studend_id: 3,
    scores: [23, 28, 21]
  }
]

> db.students.insertMany(students)

Con la query riportata sotto, selezioniamo i documenti in cui il campo ‘scores’ di tipo array contiene almeno un elemento che è compreso fra 27 (estremo escluso) e 30 (estremo incluso).

> db.students.find({scores: { $elemMatch: {$gt: 27, $lte: 30}}})

[
  {
    _id: ObjectId("6228f38a51ccf34721fceb22"),
    studend_id: 2,
    scores: [ 26, 28, 30 ]
  },
  {
    _id: ObjectId("6228f38a51ccf34721fceb23"),
    studend_id: 3,
    scores: [ 23, 28, 21 ]
  }
]

L’operatore $elemMatch può risultare anche utile nel caso si abbia a che fare con un array di documenti annidati e debbano essere soddisfatte più condizioni contemporaneamente sui campi di questi ultimi.

Facendo riferimento alla collezione grades del database sample_training, possiamo ottenere i documenti in cui l’array scores contiene almeno un documento annidato nel quale è presente contemporaneamente un campo type pari a "exam" e un campo score superiore a 99.98.

db.grades.find(
  {
    scores: {
      $elemMatch: { 
        type: 'exam', score: { $gt: 99.98}
      }
    }
  }
)

Cursori

Come detto in precedenza, il metodo db.collection.find() restituisce un cursore. Abbiamo già visto alcuni dei metodi del cursore, ma ne esistono altri che possono essere altrettanto utili.

Possiamo salvare un riferimento al cursore restituito da db.collection.find() in una variabile ed invocare uno dei suoi metodi successivamente.

var cursor = db.restaurants.find( {borough: 'Brooklyn'})

Un cursore presenta due metodi hasNext() e next() che permettono di scorrere i documenti restituiti da una query.

> var cursor = db.restaurants.find( {borough: 'Brooklyn'})
> while (cursor.hasNext()) {
  var doc = cursor.next();
  console.log(doc.name);
}

In alternativa possiamo anche usare un ciclo attraverso il metodo forEach():

> var cursor = db.restaurants.find( {borough: 'Brooklyn'})
> cursor.forEach(doc => console.log(cursor.name))

Il metodo cursor.limit()

Per impostare il numero massimo di documenti da restituire utilizzeremo invece cursor.limit(<numero-di-documenti>) che è simile all’istruzione LIMIT in un database SQL.

Possiamo appendere direttamente cursor.limit() al metodo db.collection.find()

db.restaurants.find(
  {borough: 'Brooklyn'}, 
  {name: 1, borough: 1, cuisine: 1}
).limit(3)

Il metodo cursor.skip()

Il metodo cursor.skip(<n>) è utile se vogliamo trascurare i primi n (argomento del metodo skip) documenti che soddisfano i criteri di selezione di una query e ricevere solo i restanti.

db.restaurants.find(
  {borough: 'Brooklyn'}, 
  {name: 1, borough: 1, cuisine: 1}
).skip(10).limit(5)

Il metodo cursor.sort()

Il metodo cursor.sort() serve ad ordinare i documenti restituiti da una query. L’argomento da passare è un oggetto le cui chiavi indicano in base a quali campi vogliamo ordinare il risultato. Il valore che assegniamo a ciascun campo è 1 per ordinare i documenti in modo ascendente secondo quel campo oppure -1 per un ordinamento decrescente.

db.restaurants.find(
  {borough: 'Brooklyn'}, 
  {name: 1, borough: 1, cuisine: 1}
).skip(10).limit(50).sort({name: 1, cuisine: -1})

La query riportata sopra restituisce 50 ristoranti di Brooklyn ordinati in base al nome in ordine crescente e al tipo di cucina in ordine decrescente. Nel risultato includiamo solo i campi: ‘name’, ‘borough’ e ‘cuisine’.

Prima di eseguire delle operazioni di ordinamento può essere utile configurare delle regole in base alla lingua dei dati. Trovate maggiori informazioni sulla documentazione ufficiale.

Nella prossima lezione…

Dopo aver spiegato come effettuare delle query attraverso i metodi db.collection.findOne() e db.collection.find(), nella prossima lezione vedremo come ottimizzare le query attraverso gli indici.

Pubblicitร