Läs och skriv till MongoDB med C# driver

I vårt projekt med Fältrapportering med TC55 så har jag nått fram till att läsa våra undersökningar från, och skriva ner våra rapporter till, MongoDB. Det är detta som kallas CRUD, vilket är en förkortning för Create, Read, Update, Delete, det vill säga Skapa, Läsa, Uppdatera, Ta bort. Eftersom jag är en C#-utvecklare så använder jag MongoDB C# drivers.

Installera MongoDB C# driver

Ända sedan Visual Studio 2010 så ska din första instinkt för att hämta kodbibliotek vara att kolla NuGet. Det gäller givetvis i MongoDB C# driver-fallet också. Öppna NuGet och sök efter MongoDB. Bland de översta träffarna (den översta när denna blog skrivs) ska vara Official MongoDB C# driver och det är bara att klicka på installera så är allt redo.

MongoDB är väl indelat i många olika namespace:s så var beredd på du kommer behöva lägga till nya using för att intellisense:n ska fungera för mycket av det du kommer använda. De första intressanta objekten ligger i MongoDB.Driver så en using MongoDB.Driver; löser de omedelbara problemen.

Uppkoppling mot MongoDB

I vårt projekt har jag än så länge inte stött på något behov av att använda något annat än MongoDB standard, så även om uppkopplingen mot MongoDB har många avancerade inställningsmöjligheter så går jag inte in på de här.

Första steget är att skapa en MongoClient, som är objektet som hanterar uppkopplingen mot MongoDB. Den hanterar de samlade uppkopplingarna (Connection Pooling) och övrig uppkopplingslogik.

    var client = new MongoClient("mongdb://test:test@localhost/test");

I exemplet ovan använder jag det sättet att skapa en MongClientden med en URL till databasen. URL:en skrivs på formatet

mongodb://<username>:<password>@<hostname>/<database>

Därefter hämtar vi ut en MongoServer ur MongoClient, vilket skapar en ny uppkoppling med de inställningarna som är registrerade i MongoClient.

    var server = Client.GetServer();

Och till sist behövs en MongoDatabase som hämtas ur MongoServer.

    var database = server.GetDatabase("test");

Nu är vi klara att börja läsa och skriva till MongoDB.

CRUD med MongoCollection

Kort om MongoDB som dokumentsdatabas

MongoDB är en dokumentsdatabas. Som relativ nybörjare i området så är jag inte säker på allt vad det betyder, men i grunden så dokumentsdatabaser anpassade för att lagra objekt som är grupperade på något specifikt sätt. Det kan vara ett faktiskt dokument som lagras grupperade med Titel, innehåll, kommentarer, författare och så vidare, där allt lagras i en djup struktur med klasser och underklasser. I C# världen skulle den kunna se ut såhär

public class Memo
{
    public string Title { get; set; }
    public string Body { get; set; }
    public Author Author { get; set; }
    public List<Comment> Comments { get; set; }
}

När ett dokuement som ovan lagras i MongoDB, så kommer MongoDB automatiskt skapa upp ett _id i databasen med ett automatiskt genererat 12-bytes GUID. Det är bara rotobjektet som får detta GUID, det vill säga att Memo-dokumentet kommer få ett _id men inte Author eller Comments. För att nå id:t i din klass så är det enklast att använda sig av datatypen ObjectId. I det fallet klarar C# drivers av att serialisera id-fältet åt dig

public class Memo
{
    public ObjectId Id { get; set; }
    ...
}

Namnet kan vara Id, id eller _id och serialiseringen kommer att fungera utan problem.

Dags att läsa och skriva

Att läsa och skriva till MongoDB sker via en MongoCollection. En Collection i MongoDB är en samling dokument och motsvarar löst en tabell i en relationsdatabas. Om du skapar en collection med den generiska funktionen så får du en MongoCollection som klarar av automagisk översättning av dina klasser till BSON som MongoDB förstår (BSON är en binär representation av ditt data i ett format liknar JSON). Det innebär att om du använder den generiska versionen av MongoCollection så är det bara att skicka in dina objekt och låta MongoDB hantera serialisering och deserialisering till MongoDB. För att få tag på en MongoCollection så skapar du den via MongoDatabase.

    public class Author
    {
        public ObjectId Id { get; set; } 
        public string Name { get; set; }
    }

    // Stoppa in namnet på din Collection i GetCollection
    var collection = database.GetCollection<Author>("Author");

För CRUD-operationerna (samt möjligheten att hämta alla objekt) så gör du följande:

    // Insert (Create)
    public void Insert(Author author)
    {
        collection.Insert(author)
    }

    // Get (Read)
    public void Get(ObjectId Id)
    {
        return collection.FindOneById(Id);
    }

    // Update
    public void Update(Author author)
    {
        collection.Save(author);
    }

    // Delete
    public void Delete(Author author)
    {
        var query = Query.EQ("_id", author.Id);
        collection.Remove(query);
    }

    // Get all
    public void GetAll()
    {
        collection.FindAll();
    }

Delete använder sig av en MongoQuery. Om våra behov blir mer avancerade framöver så kanske jag går in på hur den fungerar.

I nästa del så beskriver jag hur jag byggde upp klasstrukturen kring CRUD-operationerna och uppkopplingen till MongoDB.

Förenkla LINQ to Entity Framework med Extensions Methods del 2

Detta är en fortsättning på https://dev.datema.se/2014/06/04/forenkla-linq-to-entity-framework-med-extensions-methods-del-1/

Filtrera vid behov

Eftersom LINQs underliggande struktur är ett träd, så är det väldigt enkelt att klistra på metoder där de behövs. I vår kod så har jag använt detta för att lägga på filter vid behov. OBS! Följande sätt att peta in Where lite här och där i LINQ-uttrycken tenderar att skapa horribla SQL-frågor. Om du jobbar med stora datamängder så är det mycket möjligt att detta kommer orsaka frågor som tar lång tid.

public class CoreBase
{
    public int CustomerId { get; set; }
    protected internal IQueryable _customers
    {
        get 
        {
            if (CustomerId > 0)
                return _context.Customers.Where(x => x.Id == CustomerId);
            return _context.Customers;
        }
    } 

    public int AdministratorId { get; set; }
    protected internal IQueryable _people
    {
        get
        {
            if (AdministratorId > 0)
                return _context.People.Where(x => x.Id == AdministratorId);
            return _context.People;
        }
    }
}

Jag har exponerat flertalet av mina DbSet på detta sätt. När jag exponerar metoder till övre lager för att exempelvis läsa ordrar så är det väldigt enkelt att lägga på filter för att endast läsa alla ordrar för en viss kund.

    core.CustomerId = 3; 
    var orders = core.GetOrders();
    
    // och i coreklassens basklass 
    public List GetOrders() 
    { 
        return (from o in _orders
                join c in _customers on o.CustomerId equals c.Id
                select o).ToList();
    }

Ogres and code is like onions, it has got layers

Vi använder Entity Framework Code First i projektet, så vi låter Entity Framework generera databasen åt oss. Mellan Entity Framework och databasen finns en ADO-adaptern som sköter översättningen av LINQ-uttrycken till SQL. ADO-Adaptern för SQLite stödjer inte sådant som att skapa databaser och ändra tabeller. Vi löste det problemet med arv, där vår basklass har definitionen av våra DbSet och diverse stödfunktioner, och all databasspecifik kod ligger i de ärvda klasserna.

    public abstract class ContextBase : DbContext
    {
        public DbSet<Customer> Customers { get; set; }
        ...
    }

    public class SqlContext : ContextBase
    {
        ...
        static SqlContext()
        {
            Database.SetInitializer<RegistryContext>(new CreateInitializer());
        }

        class CreateInitializer : CreateDatabaseIfNotExists<RegistryContext>
        {
            ...
        }
    } 

    public class SQLiteContext : ContextBase
    {
        ... 
        static SqlContext()
        {
            Database.SetInitializer<ClientRegistryContext>(new CreateSqlLiteInitializer());
        }

        public class CreateSqlLiteInitializer : IDatabaseInitializer<ClientRegistryContext>
        {
            public void InitializeDatabase(ClientRegistryContext context)
            {
                // I SQLite fallet så skapar vi själva upp tabellerna med SQL
                // via SqlQuery på context.Database
                context.Database.SqlQuery("CREATE TABLE...");
                ...
            }
        }
    }

Det ger mig fördelen att jag kan enkelt kontrollera om en viss Context används av SQL eller SQLite.

Entity Framework med SQLite

Att använda Entity Framework med MS-SQL och SQLite går generellt sett bra, men SQLite har problem med vissa mer komplexa LINQ-uttryck. Två punkter va viktiga för mig att tänka på för att hitta en lämlig lösning. Det ena är att LINQ mot databaser är i stort sett en delmängd av funktionaliteten av det som kan användas i LINQ to Objects. Med andra ord så borde alla LINQ-uttryck som resulterar i databasanrop, ge exakt samma resultat om de istället jobbar mot t ex listor. Den andra punkten är att i detta projekt så innehåller SQLite databaserna endast en liten den av den stora databasen, och vi har inte något fall en där datamängderna är större än några tusen rader. Med de kunskaperna i åtanke så beslutade jag att ta kostnaden att läsa allt data i minnet och använda LINQ to Objects istället. Lösningen blev att i min basklass som jag redan skapat för dessa mer komplicerade frågor så la jag till en till Extension Method för att läsa upp data i minnet om det inte är ett anrop mot MS-SQL. Det sker enkelt via en .ToList(), och en .AsQueryable() för att exponera listan som en IQueryable.


    public static class CombinedCoreExtension
    {
        public static IQueryable<T> Transform<T>(this IRegistryContext context, IQueryable<T> source)
        {
            // Om frågan är mot en MS-SQL-databas så behövs ingen förändring
            if (context is RegistryContext)
                return source;

            // Om det är mot SQLite så läs upp i minnet.
            return source.ToList().AsQueryable();
        }
    }

Sedan ändrade jag min Core-bassklass så den använder denna nya Extension Method.

    protected internal IQueryable _customers
    {
        get 
        {
            // Här lades Transform till
            if (CustomerId > 0)
                return _context.Transform(context.Customers).Where(x => x.Id == CustomerId);
            return _context.Transform(context.Customers);
        }
    } 

Tester är bra, inte bara för testning

Om du har läst del 1 så kanske du undrar över de Extension Methods som jag skapade där. De borde orsaka NullReferenceException när en List försöks cast:as till DbSet. Mina exempel va förenklingar för att få de lite kortare. Metoderna ser egentligen ut på sätt:

    public static class CustomerExtensions
    {
        public static IQueryable EagerlyLoadOrdersRowsAndPeople(this IQueryable source)
        {
            if (s is DbSet)
                return (source as DbSet).Include(x => x.Orders.Select(x => x.Rows)).Include(x => x.People);
            return source;
        }
    }
 

Vi gör på det sättet eftersom i våra enhetstester så använder vi inte Entity Framework utan använder minnesstrukturer.

Förenkla LINQ to Entity Framework med Extensions Methods del 1

Den stora fördelen med Entity Framework är LINQ to Entities. Det är extremt enkelt att modulärt bygga på IQueryable vid behov. Till exempel så är det inte ovanligt att vilja läsa in refererade entities direkt (Eagerly Load) som normalt laddas in vid behov (Lazy Load). För att göra detta i Entity Framework så används Include-funktionen

    Context.Customers.Include(x => x.Orders)

I detta enkla scenario är det inte svårt att förstå vad som ska hända, men i ett mer krångligt scenario så blir koden mindre uppenbar

    // För att automatiskt läsa in ett länkat objekts 
    // egna länkade object så använd Select inne i Include
    Context.Customers.Include(x => x.Orders.Select(x => x.Rows)).Include(x => x.People)

Vad jag då hellre ser är en förklarade text för vad jag vill göra.

    Context.Customers.EagerlyLoadOrdersRowsAndPeople()

Detta är busenkelt med en eller flera Extension Methods.

public static class CustomerExtensions
{
    public static IQueryable<Customer> EagerlyLoadOrdersRowsAndPeople(this IQueryable<Customer> s)
    {
        var customers = (DbSet<Customer>)s;
        return customers.Include(x => x.Orders.Select(x => x.Rows)).Include(x => x.People);
    }
}

Det går även bra attt bygga upp dessa av varandra

public static class CustomerExtensions
{
    public static IQueryable<Customer> EagarlyLoadOrdersRowsAndPeople(this IQueryable<Customer> s)
    {
        return s.EagarlyLoadOrdersAndRows().EagarlyLoadPeople();
    }
    
    public static IQueryable<Customer> EagarlyLoadOrdersAndRows(this IQueryable<Customer> s)
    {
        var customers = (DbSet<Customer>)s;
        return customers.Include(x => x.Orders.Select(x => x.Rows));
    }
    
    public static IQueryable<Customer> EagarlyLoadPeople(this IQueryable<Customer> s)
    {
        var customers = (DbSet<Customer>)s;
        return customers.Include(x => x.People);
    }
}

Write your own tests

During a conversation about the lovely library called Dapper and CRUD I was made aware of the existence of an object called TransactionScope, which MSDN describes as:

Makes a code block transactional.

Big words hinting at magic.

I immediately started wondering what the difference was, compared to SqlTransaction, which is the object that most people are accustomed to working with. As the conversation continued someone mentioned that TransactionScope had a huge performance hit compared to SqlTransaction during bulk operations, or so they’d read on Stack Overflow (http://goo.gl/HSEf65).

Apparently the poster was claiming that 50 000 inserts where taking around 20 seconds to finish using TransactionScope and only around 4 seconds when using SqlTransaction.

In some cases that’s the end and most people will ignore TransactionScope and just use SqlTransaction instead, I however chose to do my own tests. I created a database with a table both localy and on one of our servers, wrote my own tests and timed it. The results didn’t match up with the ones posted by the poster.

The poster claimed the following:

  • TransactionScope: 2 500 inserts/sec
  • SqlTransaction: 12 500 inserts/sec

My tests gave me these results:

  • TransactionScope (local db): 10 200 inserts/sec
  • SqlTransaction (local db): 10 250 inserts/sec
  • TransactionScope (server db): 740 inserts/sec
  • SqlTransaction (server db): 760 inserts/sec

TransactionScope gave me the same performance as SqlTransaction, in some cases it was faster.

I’m not saying the poster lied, he probably got those results at the time; in fact I’m sure of it. How can I be sure of it? If you know how to use TransactionScope you’ll have noted that he opened his connection before he created the TransactionScope, which means that the connection was never associated with the TransactionScope (Execute does not open the connection).

What I’m saying is this: Write a snippet and test stuff for yourself, don’t take other people’s test results at face value.