MongoDB: CRUD per studiare la "migrazione" al NoSQL

MongoDB: CRUD per studiare la “migrazione” al NoSQL

MongoDB è entrato nella mia vita quasi per caso verso la fine di settembre e nell’ultimo periodo ho cominciato ad utilizzarlo migrando le API di una mia WebApp da MySQL al mondo NoSQL. La scorsa settimana mi è sorto un dilemma ed ha trovato subito una risposta:

Come verrebbe (parlando ad un livello ipotetico) la WebApp realizzata al mio nutrizionista se migrassi le API a NETCore e cambiassi l’accesso al datasource da Table Storage a MongoDB?

La WebApp è molto semplice a livello concettuale.

  • Lui: Inserimento Anagrafica Pazienti tramite WebApp
  • Lui: Inserimento Appuntamenti tramite WebApp
  • API: Notifica a “paziente” l’appuntamento

Non volendo utilizzare un database a pagamento (e non sapendo all’inizio l’utilizzo vero come carico di lavoro) la scelta del Table Storage. Le limitazioni del Table Storage per chi l’ha utilizzato sono note. A due anni di distanza non rifarei la stessa scelta.

Il codice che segue è il mio caso di studio utilizzato sia per “valutare un piano B per il futuro con nuovi strumenti” e (soprattutto) per sperimentare su del codice che già conosco una variante.

Nota Importante: Personalmente utilizzo la versione FREE di MongoDB offerta da Cloud Atlas scegliendo come Cloud Azure nella region West Europe (Netherlands) dove -in aggiunta- risiedono tutti i miei servizi cloud.

CRUD

Quanto segue è parte di progetto Azure Functions scritto in NETCore - 3.1. In questo articolo ho omesso tutta la parte di dependency injection e relativo utilizzo delle stesse nei costruttori. Quando troverete _collection sappiate che per utilizzarla dovrete acccere al vostro MongoDB e relativa collezione interessata.

CalendarDTO

In questo caso -trovandomi nel mondo NoSQL- mi sono posto una semplice domanda:

Cosa devo memorizzare che serve realmente?

La risposta è semplice

  • Dati del Paziente
  • Lista degli appuntamenti (come date)

A questo punto il DTO si è scritto da solo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System;
using System.Collections.Generic;


public class CalendarDTO
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string _id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
    public List<DateTime> RefDates { get; set; }
}

Come avrete visto è di una semplicità estrema. Ora non resta che vedere come utilizzarlo.

Create - InsertManyAsync

Per potere lavorare il sistema ha bisogno di potere effettuare il CREATE di almeno un paziente. Per farlo ho utilizzato InsertManyAsync all’interno della mia collection per salvare una lista di items da me creata in precedenza

1
2
3
4
public async Task CreateAsync(List<CalendarDTO> items)
{
    await _collection.InsertManyAsync(items);
}

STRESS TEST: Ho effettuato diversi test passando items in ingresso dai 10000 ai 90000 elementi (ogni paziente aveva dentro dai 10 ai 20 elementi in RefDates). Il tutto è andato via liscio senza problemi

Read

Una volta inseriti i dati ho effettuato due modalità di READ all’interno del codice:

  • Collezione completa
  • Collezione filtrata su un valore di RefDates

GetListAsync - Elenco Completo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public async Task<List<CalendarDTO>> GetListAsync()
{
    List<CalendarDTO> items = new List<CalendarDTO>();

    using (IAsyncCursor<CalendarDTO> cursor = await _collection.FindAsync(x => true))
    {
        while (await cursor.MoveNextAsync())
        {
            IEnumerable<CalendarDTO> batch = cursor.Current;
            items.AddRange(batch);
        }
    }

    return items.OrderByDescending(x => x.FirstName).ToList();
}

GetListAsync - Intervallo di Date

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public async Task<List<CalendarDTO>> GetListAsync(DateTime refDateFrom, DateTime refDateTo)
{
    refDateFrom = refDateFrom.Date;
    refDateTo = refDateTo.Date;

    var filterGt = Builders<CalendarDTO>.Filter.Gt("RefDates", refDateFrom);
    var filterLt = Builders<CalendarDTO>.Filter.Lt("RefDates", refDateTo);

    List<CalendarDTO> items = new List<CalendarDTO>();

    using (IAsyncCursor<CalendarDTO> cursor = await _collection.FindAsync<CalendarDTO>(filterGt & filterLt))
    {
        while (await cursor.MoveNextAsync())
        {
            IEnumerable<CalendarDTO> batch = cursor.Current;
            items.AddRange(batch);
        }
    }

    return items.OrderByDescending(x => x.FirstName).ToList();
}

STRESS TEST: Di questo stress test ho salvato i dati nel caso di 20000 e 30000 elementi. Chiamare GetListAsync senza filtri richiede circa 5-7 secondi. Nel caso di chiamata a GetListAsync passando le date come filtro (giorno, giorno+1) il tutto si riduce ad una decina di centesimi. Ci tengo a precisare che la data su cui effettuo il filtro l’ho inserita in una decina di items e basta. Questo test è più che superato!

Update

Quale Update ha più senso effettuare nella prova? L’ultima modifica che ho effettuato nella versione Table Storage riguarda la cancellazione di tutti gli appuntamenti passati. Purtroppo chi ha lavorato con Table Storage conosce benissimo le problematiche di ricerca e performance delle stesse.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public async Task RemoveOldRefDates(int monthToSave)
{
    var items = await GetListAsync(DateTime.MinValue, DateTime.UtcNow.AddMonths(-monthToSave));

    foreach (var item in items)
    {
        int count = item.RefDates.Count;
        item.RefDates = item.RefDates.Where(x => x.Date > DateTime.UtcNow.AddMonths(-monthToSave)).OrderBy(x=> x).ToList();

        var filter = Builders<CalendarDTO>.Filter.Eq("_id", item._id);
        var update = Builders<CalendarDTO>.Update.Set("RefDates", item.RefDates);

        _collection.UpdateOne(filter, update);
    }
}

Essendo il filtro mirato sul singolo item ho dovuto utilizzare UpdateOne scorrendo la collezione di items.

STRESS TEST: Questo Stress Test mi ha lasciato “deluso ma non troppo”. Utilizzando il codice proposto lo sporco lavoro di rimozione delle date viene effettuato correttamente. Come step ulteriore ho provato a cambiare da foreach a Parallel.For e qua ho vinto una bellissima Exception saturando la coda di MongoDB. Penso (e spero) sia legato al fatto di utilizzare la versione FREE.

Delete

Per concludere il CRUD manca l’operazione di Delete che vi illustro a breve

1
2
3
4
5
public async Task DeleteItem(string id)
{
    var filter = Builders<CalendarDTO>.Filter.Eq("_id", id);
    await _collection.DeleteOneAsync(filter);
}

STRESS TEST: Dopo il risultato del precedente test ho preferito utilizzare DeleteOneAsync e scrivere la cancellazione del singolo.

Conclusione

Sono soddisfatto di questa prova? SI! Non avevo mai lavorato con un NoSQL e sono felice di averlo fatto. La fase di studio è giunta al termine e dubito di cambiare la WebApp in questione vista la mole di dati presente al suo interno. Sino a quando non collassa su di essa resterà attiva la versione attuale. Nel caso il Core delle API è pronto (con una serie di personalizzazioni da effettuare)

Lascia un tuo Feedback!

Hai letto l’articolo sino in fondo? Vuoi raccontare la tua esperienza? Oppure semplicemete porre un quesito? Puoi farlo tranquillamente sul mio profilo linkedin nel post MongoDB: CRUD per studiare la “migrazione” al NoSQL unendoti ai commenti gia’ presenti.