Premiers pas avec Azure DocumentDb

Au cours du mois d’Octobre 2014, Microsoft a mis à disposition une version préliminaire de DocumentDb. Ce service fournit une base de données NoSQL orientée documents assortie de fonctionnalités supplémentaires. Parmi celles-ci, on peut trouver :
– Le support de requêtes de type SQL
– Le support des transactions
– L’intégration profonde de JSON pour le stockage des données
– Le support de Javascript comme langage intégré au moteur

Cet article montre comment gérer ce service dans le portail Azure ainsi que l’utilisation des fonctionnalités (communes) de lecture/écriture de données.

Créer une base de données DocumentDb dans le portail Azure

Tout d’abord, il faut accéder à la version Preview du portail Azure à l’adresse http://portal.azure.com. DocumentDb n’est en effet pas intégré dans le portail actuel.
La création du service est décrite en images ci-dessous.


Le service étant actuellement en Preview, le mode de tarification est fixé à Standard, et seules 3 localisations sont disponibles : Europe de l’Ouest, Europe du Nord et Ouest des Etats-Unis.

La creation du compte de stockage DocumentDb prend un peu de temps : Microsoft annonce 10 minutes ou plus. Dans le cas du compte utilisé dans cet article, elle a pris 12 minutes. A la fin de cette étape, une notification informe du succès de l’opération.

La visualisation du compte de stockage créé fournit différentes informations. Parmi elles, on trouve différentes statistiques d’utilisations ou la liste des bases de données qu’il contient. La tuile Keys permet également d’accéder aux clés d’authentification primaire et secondaire du service.

La barre d’action permet de créer une base de données liée au compte.

De la même manière, la barre d’action affichée à la visualisation de la base de données permet la création d’une collection. Les collections sont l’équivalent des tables d’une base de données relationnelle : ce sont ces éléments qui contiennent les données. A la différence des tables, les éléments que les collections contiennent peuvent être hétérogènes, c’est-à-dire que plusieurs structures de données différentes peuvent cohabiter dans chaque collection. Les procédures stockées, triggers et UDF (User-Defined functions) sont également définies au niveau des collections.

Maintenant que le compte de stockage est créé, il est temps de jouer avec le SDK client.

Les doigts dans le code

La première étape est l’ajout du SDK au projet. Il est fourni sous la forme d’un package NuGet dont le nom est Microsoft.Azure.Documents.Client. Attention, il est nécessaire d’inclure les versions préliminaires dans la recherche !

Une fois le package ajouté, nous allons importer les namespaces du SDK.

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client; 
using Microsoft.Azure.Documents.Linq;

Le point d’entrée du SDK est matérialisé par la classe Microsoft.Azure.Documents.DocumentClient. Il est nécessaire de fournir l’URI d’accès au service ainsi qu’une clé d’authentification. Pour rappel, ces deux éléments peuvent être affichés dans le portail Azure en sélectionnant la tuile Keys associée au compte DocumentDb.

string serviceEndpoint = "https://sputier.documents.azure.com:443/";
const string authKey = "<votre clé d'authentification ici>";
var DocumentClient = new DocumentClient(new Uri(serviceEndpoint), authKey);

Les méthodes GetDatabaseAccountAsync et ReadDatabaseFeedAsync permettent respectivement de lister les propriétés du compte DocumentDb et de lister les bases de données qu’il contient.

DatabaseAccount account = await DocumentClient.GetDatabaseAccountAsync();
FeedResponse<Database> dbList = await DocumentClient.ReadDatabaseFeedAsync();
foreach (var db in dbList)
{
    Console.WriteLine("Id = {0}", db.Id);
    Console.WriteLine("ResourceId = {0}", db.ResourceId);
    Console.WriteLine("CollectionsLink = {0}", db.CollectionsLink);
}

Les bases de données peuvent être créées et supprimées à l’aide des methodes CreateDatabaseAsync et DeleteDatabaseAsync.

var docsDb = dbList.FirstOrDefault(db => db.Id == "docs");
//Si la base de données existe, on la supprime et on la recrée
if (docsDb != null)
{
    //La propriété Database.SelfLink contient l'URL d'accès à la base
    await DocumentClient.DeleteDatabaseAsync(docsDb.SelfLink);

    docsDb = await DocumentClient.CreateDatabaseAsync(new Database() { Id = "docs" });
}

Le même mode de fonctionnement est utilisé pour manipuler les collections d’une base de données.

//La propriété CollectionsLink contient l'URL racine des collections de la base
var collectionList = await DocumentClient.ReadDocumentCollectionFeedAsync(docsDb.CollectionsLink);
foreach (var coll in collectionList)
{
    Console.WriteLine("Id = {0}", coll.Id);
    Console.WriteLine("ResourceId = {0}", coll.ResourceId);
}

var NHLTeamsCollection= collectionList.FirstOrDefault(coll => coll.Id == " NHLTeamsCollection");
//Si la collection existe, on la supprime et on la recrée
if (NHLTeamsCollection!= null)
{
    //La propriété SelfLink contient l'URL d'accès à la collection
    await DocumentClient.DeleteDocumentCollectionAsync(NHLTeamsCollection.SelfLink);

    NHLTeamsCollection= await DocumentClient.CreateDocumentCollectionAsync(docsDb.SelfLink, new DocumentCollection { Id = " NHLTeamsCollection" });
}

Un document peut être représenté par n’importe quel objet serialisable, dont le type est potentiellement anonyme. Il peut contenir d’autres objets, potentiellement sous la forme de collections. Pour ajouter un document à la collection de documents, il suffit d’utiliser la méthode CreateDocumentAsync de l’objet DocumentClient.

var bostonTeam = new
{
    Name = "Boston Bruins",
    Creation = 1924,
    Conference = "Eastern Conference",
    Division = "Atlantic Division",
    Players = new dynamic[] 
    {
        new { Name = "Matt Bartkowski", Country = "United States", Position = "Defenceman" },
        new { Name = "Patrice Bergeron", Country = "Canada", Position = "Centre" },
        new { Name = "Gregory Campbell", Country = "Canada", Position = "Centre" },
        new { Name = "Zdeno Chara", Country = "Slovakia", Position = "Defenceman" },
        new { Name = "Loui Eriksson", Country = "Sweden", Position = "Right Winger" },
        new { Name = "Matt Fraser", Country = "Canada", Position = "Right Winger" },
        new { Name = "Simon Gagné", Country = "Canada", Position = "Right Winger" },
        new { Name = "Seth Griffith", Country = "Canada", Position = "Right Winger" },
        new { Name = "Dougie Hamilton", Country = "Canada", Position = "Defenceman" },
        new { Name = "Chris Kelly", Country = "Canada", Position = "Left Winger", Status = "Alternate Captain" },
        new { Name = "David Krejci", Country = "Czech Republic", Position = "Center", Status = "Alternate Captain" },
        new { Name = "Torey Krug", Country = "United States", Position = "Defenceman" },
        new { Name = "Milan Lucic", Country = "Canada", Position = "Left Winger" },
        new { Name = "Brad Marchand", Country = "Canada", Position = "Left Winger" },
        new { Name = "Adam McQuaid", Country = "Canada", Position = "Defenceman" },
        new { Name = "Kevan Miller", Country = "United States", Position = "Defenceman" },
        new { Name = "Joseph Morrow", Country = "Canada", Position = "Defenceman" },
        new { Name = "Daniel Paille", Country = "Canada", Position = "Left Winger" },
        new { Name = "Tuukka Rask", Country = "Finland", Position = "Goalie" },
        new { Name = "Marc Savard", Country = "Canada", Position = "Centre" },
        new { Name = "Dennis Seidenberg", Country = "Germany", Position = "Defenceman" },
        new { Name = "Reilly Smith", Country = "Canada", Position = "Right Winger" },
        new { Name = "Carl Soderberg", Country = "Sweden", Position = "Centre" },
        new { Name = "Niklas Svedberg", Country = "Sweden", Position = "Goalie" },
        new { Name = "Zach Trotman", Country = "United States", Position = "Defenceman" },
        new { Name = "David Warsofsky", Country = "United States", Position = "Defenceman" },
    }
}; 
var buffaloTeam = new
{
    Name = "Buffalo Sabres",
    Creation = 1970,
    Conference = "Eastern Conference",
    Division = "Atlantic Division",
    Players = new dynamic[] 
    {
        new { Name = "André Benoit", Country = "Canada", Position = "Defenceman" },
        new { Name = "Nicolas Deslauriers", Country = "Canada", Position = "Left Winger" },
        new { Name = "Tyler Ennis", Country = "Canada", Position = "Left Winger" },
        new { Name = "Jhonas Enroth", Country = "Sweden", Position = "Goalie" },
        new { Name = "Brian Flynn", Country = "United States", Position = "Right Winger" },
        new { Name = "Marcus Foligno", Country = "Canada", Position = "Left Winger" },
        new { Name = "Brian Gionta", Country = "United States", Position = "Right Winger", Status = "Captain" },
        new { Name = "Zemgus Girgensons", Country = "Latvia", Position = "Centre" },
        new { Name = "Josh Gorges", Country = "Canada", Position = "Defenceman", Status = "Alternate Captain" },
        new { Name = "Matt Hackett", Country = "Canada", Position = "Goalie" },
        new { Name = "Cody Hodgson", Country = "Canada", Position = "Centre" },
        new { Name = "Patrick Kaleta", Country = "United States", Position = "Right Winger" },
        new { Name = "Andrej Meszaros", Country = "Slovakia", Position = "Defenceman" },
        new { Name = "Torrey Mitchell", Country = "Canada", Position = "Right Winger" },
        new { Name = "Matt Moulson", Country = "Canada", Position = "Left Winger", Status = "Captain" },
        new { Name = "Tyler Myers", Country = "Canada", Position = "Defenceman" },
        new { Name = "Michal Neuvirth", Country = "Czech Republic", Position = "Goalie" },
        new { Name = "Rasmus Ristolainen", Country = "Finland", Position = "Defenceman" },
        new { Name = "Sam Reinhart", Country = "Canada", Position = "Centre" },
        new { Name = "Tyson Strachan", Country = "Canada", Position = "Defenceman" },
        new { Name = "Drew Stafford", Country = "United States", Position = "Right Winger" },
        new { Name = "Cody McCormick", Country = "Canada", Position = "Centre" },
        new { Name = "Chris Stewart", Country = "Canada", Position = "Right Winger" },
        new { Name = "Mike Weber", Country = "United States", Position = "Defenceman" },
        new { Name = "Nikita Zadorov", Country = "Russia", Position = "Defenceman" },
    }
};

await DocumentClient.CreateDocumentAsync(NHLTeamsCollection.SelfLink, bostonTeam);
await DocumentClient.CreateDocumentAsync(NHLTeamsCollection.SelfLink, buffaloTeam);

Ces documents présentent les équipes professionnelles de hockey de Boston et Buffalo. Ils contiennent chacun plusieurs propriétés générales et une liste d’objets anonymes représentant ses joueurs.

La collection de documents peut être requêtée à l’aide d’une syntaxe de type SQL. Je dis bien « de type SQL » car ce n’est pas du SQL « traditionnel ». Il n’est par exemple pas possible d’effectuer une jointure entre deux collections, mais les jointures d’une collection sur elle-même sont parfaitement envisageables. Cette syntaxe est adaptée au mode de stockage des données : les données ne sont plus représentées par des enregistrements dans plusieurs tables et liés entre eux par des clés étrangères. Chaque document représente un ensemble cohérent de données stocké sous une forme hiérarchique.
Commençons par récupérer le nom de chacune des équipes. On utilise pour cela la méthode CreateDocumentQuery, qui renvoie un objet de type IQueryable.

var teamsName = DocumentClient.CreateDocumentQuery(NHLTeamsCollection.SelfLink, "SELECT Name FROM Teams");

foreach (var name in teamsName)
{
    Console.WriteLine(name);
}

On notera l’utilisation d’une clause FROM ne correspondant à aucun élément vu jusqu’alors. Le nom « Teams » fait en fait office d’alias pour la collection courante et est parfaitement arbitraire.
Le SQL de DocumentDb supporte les clauses WHERE. Nous allons donc récupérer le nom des équipes les plus anciennes à l’aide de l’opérateur <. Cet opérateur n’est utilisable que lorsque les données sont indexées en mode « Range ». Ce type d’indexation est précisé lors de la création de la collection à l’aide de la propriété IndexingPolicy de l’objet DocumentCollection. La mise en place de ce type d’indexation est faite de la manière suivante :

//Ce Path est obligatoire.
IndexingPath path1 = new IndexingPath { IndexType = IndexType.Hash, Path = "/" };
//Ce path permet d'indexer le champ Creation en mode Range.
IndexingPath path2 = new IndexingPath { IndexType = IndexType.Range, Path = @"/""Creation""/?" };

var newCollection = new DocumentCollection { Id = "NHLTeamsCollection" } ;
newCollection.IndexingPolicy.IncludedPaths.Add(path1);
newCollection.IndexingPolicy.IncludedPaths.Add(path2);

NHLTeamsCollection = await DocumentClient.CreateDocumentCollectionAsync(docsDb.SelfLink, newCollection);

Si la création de l’index n’est pas effectuée, la requête suivante génère une exception.

var oldestTeams = DocumentClient.CreateDocumentQuery(NHLTeamsCollection.SelfLink, "SELECT T.Name FROM Teams T WHERE T.Creation < 1950");
foreach (var item in oldestTeams)
{
    Console.WriteLine(item.Name);
}

Il est toutefois possible d’exécuter cette requête SQL sans que le champ Creation soit indexé en mode Range. Un paramètre doit alors être ajouté lors de la création de la requête. Ce paramètre ajoute un header à la requête http sous-jacente, ce qui permet à DocumentDb de gérer correctement l’appel.

var oldestTeams = DocumentClient.CreateDocumentQuery(NHLTeamsCollection.SelfLink, "SELECT T.Name FROM Teams T WHERE T.Creation < 1950", new FeedOptions { EnableScanInQuery = true });

DocumentDb autorise également l’exécution d’une requête sur des éléments enfants des documents de la collection. Ici, on récupére la liste de tous les joueurs à l’aide d’une clause FROM … IN …

var players = DocumentClient.CreateDocumentQuery<dynamic>(NHLTeamsCollection.SelfLink,
    "SELECT * FROM P in Teams.Players");

foreach (var player in players.ToList())
{
    Console.WriteLine(player.Name);
}

Enfin, la requête suivante utilise la clause JOIN afin de pouvoir manipuler les éléments enfants de la propriété Players dans le but de récupérer la liste des joueurs suédois ainsi que l’équipe dont ils font partie.

var swedishPlayersList = DocumentClient.CreateDocumentQuery(NHLTeamsCollection.SelfLink,
    @"SELECT P.Name, T.Name as Team
        FROM Teams T 
        JOIN P IN T.Players
        WHERE P.Country = 'Sweden'");

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

Le filtre aurait tout aussi bien pu être placé sur le statut des joueurs, afin de ne récupérer que les capitaines ou « alternate captains ».

var captains = DocumentClient.CreateDocumentQuery(NHLTeamsCollection.SelfLink,
    @"SELECT P.Name, T.Name as Team, P.Status
        FROM Teams T 
        JOIN P IN T.Players
        WHERE P.Status != null");

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

De nombreuses autres fonctionnalités sont disponibles avec ce service, comme l’utilisation de Linq pour le requêtage des collections ou la possibilité de programmer la base de données. Elles seront détaillées dans les prochains articles.

Une réflexion sur “Premiers pas avec Azure DocumentDb

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