Démarrer avec ravendb

Installation ou configuration

Instructions détaillées sur la configuration ou l’installation de ravendb.

Une simple application de console RavenDB

Pour cet exemple, nous utiliserons l’[instance Live Test RavenDB][1].

Nous allons créer ici une application console simple qui illustre les opérations les plus élémentaires :

  • Création
  • Récupération par identifiant
  • Interrogation
  • Mise à jour
  • Suppression

Commencez par créer une nouvelle solution Visual Studio et ajoutez-y un projet d’application console. Appelons-le RavenDBDemoConsole. L’utilisation de la bibliothèque cliente doit être similaire si le lecteur utilise VS Code ou son éditeur préféré.

Ensuite, nous devons ajouter les références requises. Cliquez avec le bouton droit sur le nœud Références dans le volet Explorateur de solutions et sélectionnez Gérer les packages NuGet. Naviguez en ligne pour ‘RavenDb.Client’. J’utiliserai la dernière version stable, qui est - au moment d’écrire ces lignes - 3.5.2.

Écrivons du code, d’accord ? Commencez par ajouter les instructions using suivantes :

using Raven.Client;
using Raven.Client.Document;

Ceux-ci nous permettent d’utiliser IDocumentStore et DocumentStore de RavenDB, qui est une interface et son implémentation prête à l’emploi pour se connecter à une instance RavenDB. Il s’agit de l’objet de niveau supérieur que nous devons utiliser pour nous connecter à un serveur et la [documentation RavenDB de celui-ci][2] indique qu’il est utilisé comme singleton dans l’application.

Nous allons donc continuer et en créer un, mais pour des raisons de simplicité, nous n’implémenterons pas le wrapper singleton autour de celui-ci - nous le supprimerons simplement lorsque le programme se terminera afin que la connexion soit fermée de manière propre. Ajoutez le code suivant à votre méthode principale :

using (IDocumentStore store = new DocumentStore
{
    Url = "http://live-test.ravendb.net",
    DefaultDatabase = "Pets"
})
{
    store.Initialize();
}

Comme dit au début, nous utilisons l’instance Live Test RavenDB, nous utilisons son adresse comme propriété Url du DocumentStore. Nous spécifions également un nom de base de données par défaut, dans ce cas, “Pets”. Si la base de données n’existe pas encore, RavenDB la crée en essayant d’y accéder. S’il existe, le client peut utiliser celui qui existe. Nous devons appeler la méthode Initialize() pour pouvoir commencer à travailler dessus.

Dans cette application simple, nous maintiendrons les propriétaires et les animaux domestiques. Nous pensons à leur connexion car un propriétaire peut avoir un nombre arbitraire d’animaux de compagnie, mais un animal peut n’avoir qu’un seul propriétaire. Même si dans le monde réel, un animal de compagnie peut avoir un nombre arbitraire de propriétaires, par exemple, un mari et une femme, nous opterons pour cette hypothèse car [les relations plusieurs à plusieurs dans une base de données de documents sont gérées quelque peu différemment] [3 ] de cela dans une base de données relationnelle et mérite un sujet à part entière. J’ai choisi ce domaine parce qu’il est assez courant pour être saisi.

Nous devons donc maintenant définir nos types d’objets de domaine :

public class Owner
{
    public Owner()
    {
        Pets = new List<Pet>();
    }

    public string Id { get; set; }
    public string Name { get; set; }
    public List<Pet> Pets { get; set; }

    public override string ToString()
    {
        return
            "Owner's Id: " + Id + "\n" +
            "Owner's name: " + Name + "\n" +
            "Pets:\n\t" +
            string.Join("\n\t", Pets.Select(p => p.ToString()));
    }
}

public class Pet
{
    public string Color { get; set; }
    public string Name { get; set; }
    public string Race { get; set; }

    public override string ToString()
    {
        return string.Format("{0}, a {1} {2}", Name, Color, Race);
    }
}

Il y a certaines choses à noter ici :

Premièrement, nos Owners peuvent contenir zéro ou plusieurs Pets. Notez que la classe Owner a une propriété appelée Id alors que la classe Pet n’en a pas. En effet, les objets Pet seront stockés à l’intérieur des objets Owner, ce qui est assez différent de la façon dont ce type de relation serait implémenté dans une base de données relationnelle.

On peut dire que cela ne devrait pas être implémenté comme ça - et c’est peut-être juste, cela dépend vraiment des exigences. En règle générale, s’il est logique qu’un ‘Animal de compagnie’ existe sans ‘Propriétaire’, il ne doit pas être intégré mais exister seul avec son propre identifiant. Dans notre application, nous supposons qu’un “Animal de compagnie” n’est considéré comme un animal de compagnie que s’il a un propriétaire, sinon ce serait une créature ou une bête. Pour cette raison, nous n’ajoutons pas de propriété Id à la classe Pet.

Deuxièmement, notez que l’identifiant de la classe propriétaire est une chaîne comme il est généralement indiqué dans les exemples de la documentation de RavenDB. De nombreux développeurs habitués aux bases de données relationnelles peuvent considérer cela comme une mauvaise pratique, ce qui est généralement logique dans le monde relationnel. Mais parce que RavenDB utilise Lucene.Net pour effectuer ses tâches et parce que Lucene.Net est spécialisé dans le fonctionnement avec des chaînes, c’est parfaitement acceptable ici - aussi, nous avons affaire à une base de données de documents qui stocke JSON et, après tout, fondamentalement, tout est représenté sous forme de chaîne en JSON.

Une autre chose à noter à propos de la propriété Id est qu’elle n’est pas obligatoire. En fait, RavenDB attache ses propres métadonnées à tout document que nous enregistrons, donc même si nous ne le définissions pas, RavenDB n’aurait aucun problème avec nos objets. Il est cependant généralement défini pour un accès plus facile.

Avant de voir comment nous pouvons utiliser RavenDB à partir de notre code, définissons quelques méthodes d’assistance courantes. Ceux-ci devraient être explicites.

// Returns the entered string if it is not empty, otherwise, keeps asking for it.
private static string ReadNotEmptyString(string message)
{
    Console.WriteLine(message);
    string res;
    do
    {
        res = Console.ReadLine().Trim();
        if (res == string.Empty)
        {
            Console.WriteLine("Entered value cannot be empty.");
        }
    } while (res == string.Empty);

    return res;
}

// Will use this to prevent text from being cleared before we've read it.
private static void PressAnyKeyToContinue()
{
    Console.WriteLine();
    Console.WriteLine("Press any key to continue.");
    Console.ReadKey();
}

// Prepends the 'owners/' prefix to the id if it is not present (more on it later)
private static string NormalizeOwnerId(string id)
{
    if (!id.ToLower().StartsWith("owners/"))
    {
        id = "owners/" + id;
    }

    return id;
}

// Displays the menu
private static void DisplayMenu()
{
    Console.WriteLine("Select a command");
    Console.WriteLine("C - Create an owner with pets");
    Console.WriteLine("G - Get an owner with its pets by Owner Id");
    Console.WriteLine("N - Query owners whose name starts with...");
    Console.WriteLine("P - Query owners who have a pet whose name starts with...");
    Console.WriteLine("R - Rename an owner by Id");
    Console.WriteLine("D - Delete an owner by Id");
    Console.WriteLine();
}

Et notre méthode principale :

private static void Main(string[] args)
{
    using (IDocumentStore store = new DocumentStore
    {
        Url = "http://live-test.ravendb.net", 
        DefaultDatabase = "Pets"
    })
    {
        store.Initialize();

        string command;
        do
        {
            Console.Clear();
            DisplayMenu();

            command = Console.ReadLine().ToUpper();
            switch (command)
            {
                case "C":
                    Creation(store);
                    break;
                case "G":
                    GetOwnerById(store);
                    break;
                case "N":
                    QueryOwnersByName(store);
                    break;
                case "P":
                    QueryOwnersByPetsName(store);
                    break;
                case "R":
                    RenameOwnerById(store);
                    break;
                case "D":
                    DeleteOwnerById(store);
                    break;
                case "Q":
                    break;
                default:
                    Console.WriteLine("Unknown command.");
                    break;
            }
        } while (command != "Q");
    }
}

Création

Voyons comment nous pouvons enregistrer certains objets dans RavenDB. Définissons les méthodes courantes suivantes :

private static Owner CreateOwner()
{
    string name = ReadNotEmptyString("Enter the owner's name.");

    return new Owner { Name = name };
}

private static Pet CreatePet()
{
    string name = ReadNotEmptyString("Enter the name of the pet.");
    string race = ReadNotEmptyString("Enter the race of the pet.");
    string color = ReadNotEmptyString("Enter the color of the pet.");

    return new Pet
    {
        Color = color,
        Race = race,
        Name = name
    };
}

private static void Creation(IDocumentStore store)
{
    Owner owner = CreateOwner();
    Console.WriteLine(
        "Do you want to create a pet and assign it to {0}? (Y/y: yes, anything else: no)", 
        owner.Name);

    bool createPets = Console.ReadLine().ToLower() == "y";
    do
    {
        owner.Pets.Add(CreatePet());

        Console.WriteLine("Do you want to create a pet and assign it to {0}?", owner.Name);
        createPets = Console.ReadLine().ToLower() == "y";
    } while (createPets);

    using (IDocumentSession session = store.OpenSession())
    {
        session.Store(owner);
        session.SaveChanges();
    }
}

Voyons maintenant comment cela fonctionne. Nous avons défini une logique C# simple pour créer des objets Owner et continuer à créer et à lui attribuer des objets Pet jusqu’à ce que l’utilisateur le souhaite. La partie qui concerne RavenDB et qui fait donc l’objet de cet article est la manière dont nous sauvegardons les objets.

Afin de sauvegarder le Propriétaire nouvellement créé avec son Pets, nous devons d’abord ouvrir une session, qui implémente IDocumentSession. Nous pouvons en créer un en appelant OpenSession sur l’objet de magasin de documents.

Alors, notez la différence, alors que le magasin de documents est un objet permanent qui existe généralement pendant toute la durée de vie de l’application, le IDocumentSession est un objet léger et de courte durée. Il représente une série d’opérations que nous souhaitons effectuer en une seule fois (ou du moins, en quelques appels à la base de données).

RavenDB insiste (et force quelque peu) sur le fait que vous évitez un nombre excessif d’allers-retours vers le serveur, ce qu’ils appellent «protection contre les bavardages client-serveur» sur le site Web de RavenDB. Pour cette raison, une session a une limite par défaut au nombre d’appels de base de données qu’elle tolère, et il faut donc faire attention au moment où une session est ouverte et supprimée. Parce que dans cet exemple, nous traitons la création d’un Owner et de ses Pets comme une opération qui doit être exécutée par elle-même, nous le faisons en une seule session, puis nous le supprimons.

Nous pouvons voir deux autres appels de méthode qui nous intéressent :

  • session.Store(owner), qui enregistre l’objet pour l’enregistrement et, en outre, définit la propriété Id de l’objet si elle n’est pas encore définie. Le fait que la propriété de l’identifiant s’appelle ‘Id’ est donc une convention.
  • session.Savehanges() envoie les opérations réelles à exécuter au serveur RavenDB, validant toutes les opérations en attente.

Récupération par identifiant

Une autre opération courante consiste à obtenir un objet par son identifiant. Dans le monde relationnel, nous le faisons normalement en utilisant une expression “Où”, en spécifiant l’identifiant. Mais comme dans RavenDB, chaque requête est effectuée à l’aide d’index, qui peuvent être [stale][4], ce n’est pas l’approche à adopter - en fait, RavenDB lève une exception si nous tentons d’interroger par identifiant. Au lieu de cela, nous devrions utiliser la méthode Load<T>, en spécifiant l’identifiant. Avec notre logique de menu déjà en place, il nous suffit de définir la méthode qui charge réellement les données demandées et affiche ses détails :

private static void GetOwnerById(IDocumentStore store)
{
    Owner owner;
    string id = NormalizeOwnerId(ReadNotEmptyString("Enter the Id of the owner to display."));

    using (IDocumentSession session = store.OpenSession())
    {
        owner = session.Load<Owner>(id);
    }

    if (owner == null)
    {
        Console.WriteLine("Owner not found.");
    }
    else
    {
        Console.WriteLine(owner);
    }

    PressAnyKeyToContinue();
}

Tout ce qui concerne RavenDB ici est, encore une fois, l’initialisation d’une session, puis l’utilisation de la méthode Load. La bibliothèque cliente RavenDB renverra l’objet désérialisé comme le type que nous spécifions comme paramètre de type. Il est important de savoir que RavenDB n’applique aucune sorte de compatibilité ici - toutes les propriétés mappables sont mappées et celles qui ne le sont pas.

RavenDB a besoin du préfixe de type de document ajouté au Id - c’est la raison de l’appel de NormalizeOwnerId. Si un document avec l’ID spécifié n’existe pas, alors null est renvoyé.

Interrogation

Nous verrons ici deux types de requêtes : une dans laquelle nous interrogeons les propriétés propres des documents “Owner” et une dans laquelle nous interrogeons les objets “Pet” intégrés.

Commençons par le plus simple, dans lequel nous interrogeons les documents Propriétaire dont la propriété Nom commence par la chaîne spécifiée.

private static void QueryOwnersByName(IDocumentStore store)
{
    string namePart = ReadNotEmptyString("Enter a name to filter by.");

    List<Owner> result;
    using (IDocumentSession session = store.OpenSession())
    {
        result = session.Query<Owner>()
           .Where(ow => ow.Name.StartsWith(namePart))
           .Take(10)
           .ToList();
    }

    if (result.Count > 0)
    {
        result.ForEach(ow => Console.WriteLine(ow));
    }
    else
    {
        Console.WriteLine("No matches.");
    }
    PressAnyKeyToContinue();
}

Encore une fois, parce que nous aimerions effectuer la requête comme un travail indépendant, nous ouvrons une session. Nous pouvons interroger une collection de documents en appelant Query<TDocumentType> sur l’objet de session. Il renvoie un objet IRavenQueryable<TDocumentType>, sur lequel nous pouvons appeler les méthodes LINQ habituelles, ainsi que certaines extensions spécifiques à RavenDB. Nous effectuons ici un filtrage simple, et la condition est que la valeur de la propriété Name commence par la chaîne saisie. Nous prenons les 10 premiers éléments du jeu de résultats et en créons une liste. Il faut faire attention à bien spécifier la taille de l’ensemble de résultats - voici une autre application défensive en jeu effectuée par RavenDB appelée Protection de l’ensemble de résultats illimité. Cela signifie que (par défaut) seuls les 128 premiers éléments sont renvoyés.

Notre deuxième requête ressemble à ceci :

private static void QueryOwnersByPetsName(IDocumentStore store)
{
    string namePart = ReadNotEmptyString("Enter a name to filter by.");

    List<Owner> result;
    using (IDocumentSession session = store.OpenSession())
    {
       result = session.Query<Owner>()
           .Where(ow => ow.Pets.Any(p => p.Name.StartsWith(namePart)))
           .Take(10)
           .ToList();
    }

    if (result.Count > 0)
    {
        result.ForEach(ow => Console.WriteLine(ow));
    }
    else
    {
        Console.WriteLine("No matches.");
    }
    PressAnyKeyToContinue();
}

Celui-ci n’est pas beaucoup plus compliqué, je l’ai écrit pour démontrer à quel point il est possible d’interroger naturellement les propriétés d’un objet intégré. Cette requête renvoie simplement les 10 premiers “propriétaires” qui ont au moins un “animal de compagnie” dont le nom commence par la valeur saisie.

Effacement

Nous avons deux options pour effectuer la suppression. L’une consiste à transmettre l’identifiant du document, ce qui est utile si nous n’avons pas l’objet lui-même en mémoire, mais nous avons l’identifiant et nous aimerions éviter un aller-retour autrement évitable vers la base de données. L’autre façon, évidemment, est de passer un objet réel enregistré à RavenDB. Nous allons examiner la première option ici, l’autre utilise simplement une autre surcharge et passe un objet approprié :

private static void DeleteOwnerById(IDocumentStore store)
{
    string id = NormalizeOwnerId(ReadNotEmptyString("Enter the Id of the owner to delete."));

    using (IDocumentSession session = store.OpenSession())
    {
        session.Delete(id);
        session.SaveChanges();
    }
}

Encore une fois, nous devons ouvrir une session pour effectuer notre travail. Comme mentionné précédemment, nous supprimons ici l’objet souhaité en passant son identifiant à la méthode Delete. Le préfixe de l’identifiant doit également être en place ici, tout comme c’était le cas avec la méthode “Load”. Pour envoyer réellement la commande de suppression à la base de données, nous devons appeler la méthode SaveChanges, qui fera exactement cela, ainsi que toutes les autres opérations en attente qui sont enregistrées dans la même session.

Mise à jour

Et enfin, nous verrons comment mettre à jour les documents. Fondamentalement, nous avons deux façons de procéder. La première est simple, nous chargeons un document, mettons à jour ses propriétés si nécessaire, puis le transmettons à la méthode Store. Cela devrait être simple selon la démonstration de chargement et de sauvegarde, mais il y a quelques points à noter.

Tout d’abord, la bibliothèque cliente RavenDB utilise un outil de suivi des modifications qui permet de mettre à jour n’importe quel document sans le transmettre à Store tant que la session qui a chargé le document est toujours ouverte. Dans ce cas, il suffit d’appeler SaveChanges sur la session pour que la mise à jour ait lieu.

Deuxièmement, pour que cela fonctionne, l’objet a évidemment besoin que son identifiant soit défini afin que RavenDB puisse déterminer ce qu’il faut mettre à jour.

Cela dit, nous n’examinerons que l’autre mode de mise à jour. Il existe un concept appelé patching, qui peut être utilisé pour mettre à jour des documents. Tout comme c’était le cas avec la suppression, il a également ses propres scénarios d’utilisation. Utiliser la méthode précédente pour effectuer une mise à jour est un bon moyen si nous avons déjà l’objet en mémoire et/ou si nous voulons utiliser sa sécurité de type. L’utilisation de correctifs est l’option si nous voulons éviter un aller-retour autrement inutile vers la base de données si nous n’avons pas déjà l’objet en mémoire. L’inconvénient est que nous perdons une partie de la sécurité du type, car nous devons spécifier les propriétés à mettre à jour en utilisant des chaînes simples (rien que certaines magies LINQ ne puissent résoudre). Voyons le code :

private static void RenameOwnerById(IDocumentStore store)
{
    string id = NormalizeOwnerId(ReadNotEmptyString("Enter the Id of the owner to rename."));
    string newName = ReadNotEmptyString("Enter the new name.");

    store.DatabaseCommands.Patch(id, new Raven.Abstractions.Data.PatchRequest[]{
        new Raven.Abstractions.Data.PatchRequest
        {
            Name = "Name",
            Value = newName
        }
    });
}

Cela conclut le tout. Vous devriez pouvoir voir l’exemple fonctionner en collant les fragments de code dans une application console.

[1] : http://live-test.ravendb.net/studio/index.html#resources [2] : https://ravendb.net/docs/article-page/3.5/csharp/client-api/creating-document-store [3] : http://stackoverflow.com/questions/20265056/many-to-many-design-for-nosql-ravendb [4] : https://ravendb.net/docs/article-page/3.5/csharp/indexes/stale-indexes