Aller un peu plus loin avec Azure DocumentDb

Mon article précédent avait pour but de montrer les fonctionnalités essentielles de Document Db. Aujourd’hui, nous allons voir comment simplifier l’accès aux données stockées dans Document Db avec le provider Linq fourni par Microsoft, puis nous verrons comment du code peut être intégré à une base Document Db au travers des procédures stockées, triggers et UDFs (User-Defined Functions). Les exemples décrits ici prennent la suite de ceux de l’article précédent : il est donc toujours question d’équipes de hockey et de leur composition.

Linq To DocumentDb

Le SDK .Net pour Azure DocumentDb inclut un fournisseur Linq qui permet l’écriture de requêtes sur la base de données en minimisant les risques d’erreurs.
Pour utiliser Linq, il est tout d’abord nécessaire de créer une structure de données correspondant au type d’objets qui doivent être récupérés. Notre scénario se base donc sur une classe Team et une classe Player.

class Team
{

    public string Name { get; set; }
    public int Creation { get; set; }
    public string Conference { get; set; }
    public string Division { get; set; }
    public List<Player> Players { get; set; }
}

class Player
{
    public string Name { get; set; }
    public string Position { get; set; }
    public string Status { get; set; }
}

La version générique de la méthode CreateDocumentQuery fournit le point d’entrée pour l’écriture de requêtes Linq, puisqu’elle retourne une collection de type IOrderedQueryable. La récupération des différentes équipes enregistrées dans la base de données peut à présent être écrit de la manière suivante :

var q = from team in DocumentClient.CreateDocumentQuery<Team>(NHLTeamsCollection.SelfLink)
        select team;

foreach (var team in q)
{
    Console.WriteLine("{0} - {1} players", team.Name, team.Players.Count());
}

L’exécution de cette portion de code affiche le résultat suivant dans la Console :

Boston Bruins - 26 players
Buffalo Sabres - 25 players

Toutes les données, incluant la liste des joueurs, sont correctement mappées et peuvent être utilisées. La totalité du mappage est effectuée en faisant la correspondance entre les noms des propriétés du document et des deux classes C#. Par ailleurs, il n’est pas obligatoire que la structure des documents et des classes C# soient identiques : la classe Player n’a pas de propriété nommée Country et tout se passe bien, et la propriété Player.Status n’est valorisé que lorsqu’un document la définit.

Programmation de la base de données

DocumentDb supporte la création de procédures stockées, de triggers et de fonctions définies par l’utilisateur (UDFs). Ces éléments sont écrits avec Javascript, ce qui simplifie grandement la manipulation de données de la base puisque celles-ci sont enregistrées au format JSON (pour rappel, JSON = JavaScript Object Notation). Leur utilisation offre également un gain de performance par le fait qu’ils sont précompilés par le moteur DocumentDb.
Alors que les bases de données traditionnelles définissent ces éléments au niveau d’une base de données, ceux-ci sont enregistrés dans DocumentDb au niveau des collections. Ceci implique que l’utilisation d’une même portion de code dans plusieurs collections nécessite une duplication de code.

User-Defined Functions (UDFs)

L’écriture de fonctions a pour intérêt l’extension du langage de requêtage de DocumentDb. Les UDFs ne peuvent en effet être utilisées que lors de la recherche de documents à l’aide de requêtes pseudo-SQL.
La spécification du SQL de DocumentDb ne contient notamment pas d’opérateur LIKE. Il est possible de créer une fonction CONTAINS qui pourrait permettre de compenser ce manque. Pour cela, on crée un objet de type UserDefinedFunction dont les propriétés Id et Body représentent respectivement le nom de la fonction et son implémentation, puis on exécute la méthode CreateUserDefinedFunctionAsync pour la créer dans la collection.

var containsUDF = new UserDefinedFunction
    {
        Id = "CONTAINS",
        Body = @"function(stringToSearch, databaseString) { 
                    return databaseString.indexOf(stringToSearch) > -1;
                };",
    };

await DocumentClient.CreateUserDefinedFunctionAsync(NHLTeamsCollection.SelfLink, containsUDF);

La fonction peut ensuite être utilisée de la manière suivante :

var playersNamedMatt = DocumentClient.CreateDocumentQuery(NHLTeamsCollection.SelfLink,
    @"SELECT P.Name, T.Name as Team
    FROM Teams T 
    JOIN P IN T.Players
    WHERE CONTAINS('Matt', P.Name) = true");

foreach (var player in playersNamedMatt.ToList())
{
    Console.WriteLine("{0} - {1}", player.Name, player.Team);
}

Attention ! La Preview de DocumentDb n’autorise l’utilisation que d’une seule UDF par requête.
Pour des raisons de performance, il est conseillé de ne pas utiliser ce type de fonction sur des volumes de données importants. En effet, DocumentDb indexe les données de manière à pouvoir effectuer des comparaisons d’égalité / différence (avec des hashs) ou des comparaisons de type « Range » : inférieur/supérieur. Le service n’est pas prévu pour l’indexation et la recherche en full-text. Pour cela, je vous conseille l’utilisation d’Azure Search Service, sur lequel je reviendrai très prochainement, qui permet la recherche en full-text.

Les procédures stockées

DocumentDb offre la possibilité d’exécuter des portions de code Javascript directement dans la base de données au travers de procédures stockées. Celles-ci peuvent effectuer des traitements plus ou moins complexes et agir sur les données des collections auxquelles elles appartiennent. Chaque exécution de procédure stockée est encapsulée dans une transaction ACID permettant ainsi l’annulation de toute modification en cas d’échec d’une opération. Ce dernier comportement permet notamment d’avoir des procédures stockées dédiées à la création transactionnelle de plusieurs enregistrements.
La forme des procédures stockées est similaire à celles des UDFs, la différence principale étant que les procédures stockées peuvent être appelées à partir de l’application cliente à l’aide de la méthode ExecuteStoredProcedureAsync. Ci-dessous, nous créons une procédure stockée permettant l’enregistrement de plusieurs documents. Le code valide l’existence de documents en paramètres ainsi que l’existence d’une propriété Id pour chacun. Si la propriété Id (obligatoire) n’est pas renseignée, un pseudo-GUID est généré et assigné.

var storedProcInsertMany = new StoredProcedure
{
    Id = "InsertMany",
    Body = @"function(documents) {
                function createGuid() {
                        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                            var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
                            return v.toString(16);
                        });
                    }

                if (!documents) {
                    throw 'Invalid argument';
                }
                                                    
                //Les 3 lignes suivantes permettent de récupérer les informations 
                //concernant le contexte d'exécution
                var context = getContext();
                var collectionManager = context.getCollection();
                var collectionLink = collectionManager.getSelfLink();

                for (var i = 0; i < arguments.length; i++) {
                    //Un document doit obligatoirement avoir une propriété id pour pouvoir être ajouté
                    //Si le document n'en a pas, on génère un pseudo-GUID pour valoriser cette propriété  
                    if (!arguments[i].id) {
                        arguments[i].id = createGuid();
                    }
                    collectionManager.createDocument(collectionLink, arguments[i]);
                }
            };"
};

await DocumentClient.CreateStoredProcedureAsync(NHLTeamsCollection.SelfLink, storedProcInsertMany);

La récupération de la procédure stockée et son exécution sont effectuées à l’aide des méthode CreateStoredProcedureQuery et ExecuteStoredProcedureAsync. L’ajout des documents bostonTeam et buffaloTeam peut être réalisé à l’aide de notre procédure stockée de la manière suivante :

try
{
    var storedProc = DocumentClient.CreateStoredProcedureQuery(NHLTeamsCollection.StoredProceduresLink)
              .Where(sp => sp.Id == "InsertMany").First();

    await DocumentClient.ExecuteStoredProcedureAsync<string>(storedProc.SelfLink, bostonTeam, buffaloTeam);
}
catch (Exception ex)
{

}

Triggers

Tout comme les procédures stockées, les triggers représentent des portions de code Javascript exécutées par le moteur DocumentDb. Ils peuvent être exécutés avant ou après une opération sur une donnée (hors sélection), mais leur exécution doit être explicitement demandée lors de l’envoi de la requête vers DocumentDb.
Le code ci-dessous crée 2 triggers, le premier étant exécuté avant l’insertion d’un document, et le second après l’insertion.

var triggerPreInsert = new Trigger()
{
    Id = "BeforeInsert",
    Body = @"function() {
                //Insérer ici le code à exécuter


                //Le document qui doit être créé peut être récupéré à partir du contexte
                //var request = context.getRequest();
                //var document = request.getBody();
                }",
    TriggerType = TriggerType.Pre,
    TriggerOperation = TriggerOperation.Create
};

var triggerPostInsert = new Trigger()
{
    Id = "AfterInsert",
    Body = @"function() {
                //Insérer ici le code à exécuter


                //Le document qui doit être créé peut être récupéré à partir du contexte
                //var response = context.getResponse();
                //var document = response.getBody();
                }",
    TriggerType = TriggerType.Post,
    TriggerOperation = TriggerOperation.Create
};

await DocumentClient.CreateTriggerAsync(NHLTeamsCollection.TriggersLink, triggerPreInsert);
await DocumentClient.CreateTriggerAsync(NHLTeamsCollection.TriggersLink, triggerPostInsert);

L’exécution est définie par l’objet RequestOptions passé lors de l’opération sur les données.

try
{
    await DocumentClient.CreateDocumentAsync(NHLTeamsCollection.SelfLink, buffaloTeam,
        new RequestOptions
        {
            //Liste des triggers à exécuter avant l'insertion
            PreTriggerInclude = new List<string> { "BeforeInsert" },
            //Liste des triggers à exécuter après l'insertion
            PostTriggerInclude = new List<string> { "AfterInsert" }
        });
}
catch (Exception ex)
{

}

Les possibilités de requètage et la programmabilité de DocumentDb en font un magasin de données adapté à certains scénarios de stockage de données. En revanche, il souffre actuellement de nombreux manques et limitations, dont : pas de requêtes paramétrées ( ! ), pas de jointures entre collections, pas d’opérateurs de tri (ORDER BY) ou d’aggrégation (COUNT, MAX, MIN…), la taille des documents actuellement limitée à 16ko ( très peu ! )… Ces limitations font que ce service ne peut actuellement convenir à tous les besoins. Il faut toutefois prendre en compte le fait que le service est actuellement une préversion et va probablement évoluer rapidement de manière à adresser ces problématiques.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s