Formation MongoDB M101N: semaine 2 – Driver .NET

Inclure le package .NuGet “MongoDB.Driver” dans son projet.
Plusieurs packages sont installés:
MongoDB.Drive, MongoDB.BSON et MongoDB.Driver.Core.
L’objet principal pour ouvrir une connection est MongoClient.
Pour entrer certains paramètres de connexion, il faut utiliser une connectionString.

var connectionString = "mongodb://localhost:27017";
var client = new MongoClient(connectionString);
var db = client.GetDatabase("test");
var col = db.GetCollection("person");

Représentation des documents

Pour représenter des documents en .NET, on utilise l’objet BsonDocument;

var doc = new BsonDocument
{
    { "name", "Jones" }
};

Pour ajouter une valeur:

doc.Add("age", 30);

Pour affecter une valeur:

doc["profression"] = "hacker";

Pour définir un tableau:

var nestedArray = new BsonArray();
nestedArray.Add(new BsonDocument("color", "red));
doc.Add("array", nestedArray);

Représentation POCO

Permet de mapper du POCO vers du Bson.
On peut utiliser directement des structures et préciser des attributs pour préciser des éléments sur des membres:
Par exemple:

class Person
{
    public ObjectId Id {get; set;}

    [BsonElement("name")]
    public string Name {get; set;}

    [BsonRepresentation(BsonType.String)]
    public int Age {get; set;}
}

Il est possible de préciser des données par code:

Par exemple:

BsonClassMap.RegisterClassMap<Person>(cm => *
{
    cm.AutoMap();
    cm.MapMember(x => x.Name).SetElementName("name");
});

Pour ajouter une convention qui permet d’indiquer que "name" doit être mappé vers le membre
Person.Name (avec une majuscule):

var conventionPack = new ConventionPack();
conventionPack.Add(new CamelCaseElementNameConvention());
ConventionRegistry.Register("camelcase", conventionPack, t => true);

insertOne

Pour insérer une valeur dans une collection:

var client = new MongoClient();
var db = client.GetDatabase("test");
var col = db.GetCollection<BsonDocument>("people");
var doc = new BsonDocument
{
    { "Name": "Smith" },
    { "Age": 30 },
};

Pour insérer un seul document:

await col.InsertOneAsync(doc);

Pour insérer plusieurs documents.

await col.InsertManyAsync();

Avec un classe mappée:

var person = class Person
{
    public ObjectId Id {get; set;}
    public string Name {get; set;}
    public int Age {get; set;}
};
var doc = new Person{ };
await col.InsertOneAsync(doc);

ATTENTION: on peut pas insérer 2 fois le même objet à cause de "_id" qui est valué.

Il faut que "_id" n’est pas de valeur.

find

find() permet de retourner plusieurs lignes comme le Shell:
Il faut passer par l’intermédiaire d’un curseur, on entoure l’utilisation du curseur avec un try...finally pour exécuter le close même en cas d’exception.

var col = db.GetCollection<BsonDocument>("people");
using (var cursor = qawait col.Find(new BsonDocument()).ToCursorAsync())
{
    while (await cursor.MoveNextAsync())
    {
        foreach (var doc in cursor.Current)
        {
            Console.WriteLine(doc);
        }
    }
}

Pour traiter une liste directement:

var list = await col.Find(new BsonDocument()).ToListAsync();

Avec un ForEach:

var list = await col.Find(new BsonDocument()).ForEachAsync(doc => ConsoleWriteLine(doc));

Avec des critères de sélection:

var filter = new BsonDocument("Name", "Smith");
var list = await col.Find(filter).ToListAsync();

Il est possible de passer du code Bson directement:

var list = await col.Find({ "Name" : "Smith" }).ToListAsync();

Pour entrer plus de conditions:

var filter = new BsonDocument("$and", new BsonArray
{
    new BsonDocument("Age", new BsonDocument("$lt", 30)), 
    new BsonDocument("Name", "Smith"),
});

var filter = builder.And(builder.Lt("Age", 30) & builder.Eq("Name", "Smith"));

Il est possible d’utiliser directement des POCO:

var col = db.GetCollection<Person>("people");

En utilisant des lambda expressions:

var list = await col.Find(x => x.Age < 30 && x.Name != "Smith").ToListAsync();

Skip

Permet de sauter des valeurs:

var list = await col.Find(new BsonDocument())
    .Limit(1)
    .Skip(1)
    .ToListAsync();

Sort

Pour trier des éléments:

var list = await col.Find(new BsonDocument())
    .Sort(new BsonDocument("{ Age : 1 }")
    .ToListAsync();

ou

var list = await col.Find(new BsonDocument())
    .Sort(new BsonDocument("Age", 1)
    .ToListAsync();

ou

var col = db.GetCollection<BsonDocument>("people");
var list = await col.Find(new BsonDocument())
    .Sort(Builders<BsonDocument>.Sort.Ascending("Age"))
    .ToListAsync();

En utilisant POCO;

var col = db.GetCollection<Person>("people");
var list = await col.Find(new BsonDocument())
    .Sort(Builders<Person>.Sort.Ascending("Age").Descending("Name"))
    .ToListAsync();

ou

var list = await col.Find(new BsonDocument())
    .SortBy(x => x.Age)
    .ToListAsync();

ou

var list = await col.Find(new BsonDocument())
    .Sort(Builders<Person>.Sort.Ascending(x => x.Age))
    .ToListAsync();

Projections

var list = await col.Find(new BsonDocument())
    .Project("{ Name : 1, _Id: 0}")
    .ToListAsync();

ou

var col = db.GetCollection<Person>("people");
var list = await col.Find(new BsonDocument())
    .Project<Person>(Builders<Person>.Projection.Include("Name").Exclude("_Id"))
    .ToListAsync();

ou

var col = db.GetCollection<Person>("people");
var list = await col.Find(new BsonDocument())
    .Project(x => x.Name)
    .ToListAsync();

Replace

var result = await col.ReplaceOneAsync(
    new BsonDocument("_Id", 5)
    new BsonDocument("_Id", 5).Add("x", 30)
);

Si on utilise les "_Id", il faut répéter le séquence.

var result = await col.ReplaceOneAsync(
    new BsonDocument("x", 5)
    new BsonDocument("x", 30)
);

Pour savoir si l’update a fait quelque chose, il faut regarder le résultat dans "result".
En ajoutant une option:

var result = await col.ReplaceOneAsync(
    new BsonDocument("x", 5)
    new BsonDocument("x", 30)
    new UpdateOptions { IsUpsert = true }
);

Update

Même type de syntaxe:

var result = await col.UpdateOneAsync(
    Builders<BsonDocument>.Filter.Eq("x", 5),
    new BsonDocument("$inc", new BsonDocument("x", 10))
);

ou

var result = await col.UpdateOneAsync(
    Builders<BsonDocument>.Filter.Eq("x", 5),
    Builders<BsonDocument>.Update.Inc("x", 10)
);

ou

var result = await col.UpdateOneAsync(
    x => x.X > 5,
    Builders<BsonDocument>.Update.Inc("x", 10)
);

UpdateMany permet de modifier plusieurs lignes.

Delete

Pour supprimer seulement 1 élément:

var result = await col.DeleteOneAsync(x => x.X > 5);

Ou pour supprimer plusieurs documents:

var result = await col.DeleteManyAsync(x => x.X > 5);

FindOneAndUpdate

Permet de faire une mise à jour si on trouve l’élément.

var result = await col.FindOneAndUpdateOneAsync(
    x => x.X > 5,
    Builders<BsonDocument>.Update.Inc("x", 10)
);

Le résultat contient le document qui a été modifié.
On peut rajouter des options:

var result = await col.FindOneAndUpdateOneAsync(
    x => x.X > 5,
    Builders<BsonDocument>.Update.Inc("x", 10),
    new FindOneAndUpdateOptions<Widget, Widget>
    {
         ReturnDocument = ReturnDocument.After
    }
);

Par défaut, on renvoie le document avant modification. L’option "ReturnDocument" permet de renvoyer le document après la modification.

Sort

Pour trier les éléments avant de les modifier:

var result = await col.FindOneAndUpdateOneAsync(
    x => x.X > 5,
    Builders<BsonDocument>.Update.Inc("x", 10),
    new FindOneAndUpdateOptions<Widget, Widget>
    {
        ReturnDocument = ReturnDocument.After,
        Sort = Builders<Widget>.Sort.Descending(x => x.X),
    }
);

Formation MongoDB M101N: semaine 2 – CRUD

Opérations CRUD et mongo Shell

Les mots clé en mongo équivalent à:
– Create: insert
– Read: find
– Update: update
– Delete: remove

Pour effectuer des opérations CRUD en mongodb, on utilise pas une string comme en SQL mais directement des fonctions de l’API.
Le mongo Shell est un shell Javascript ce qui signifie qu’il est possible d’écrire du Javascript directement dans le Shell.
Les raccourcis sont compatibles avec les raccourcis EMACS (Ctrl + A, Ctrl + E etc…).
Pour avoir de l’aide:
taper “help”

Assignation de variable:

x = 1; y = "abc";

z = { "a" : 1 } permet de créer un dictionaire. Pour accéder à une valeur, il suffit de faire z.a ou z["a"]. Il faut privilégier z["a"] qui permet d’accéder au type de la valeur.

Par exemple:

x = { "a" : 1 };
y = "a";
x[y]++;
print(x.a);

Le résultat est 2.

Afficher une valeur

print(x.a)

BSON

Les données stockées dans une base Mongo sont sous forme de données binaires: BSON pour binary JSON.
Il est possible de faire des affectations de dictionnaires et de documents intégrés (“embedded document”):

obj = { "a" : 1, "b" : "hello", "c" : [ "apples", "tomatoes" ] }

Mais il est aussi possible d’affecter des données plus fortement typées en utilisant des constructeurs:
NumberInt(1) pour des entiers codés en 16bits ou NumberLong(3) pour des entiers codés en 64 bits.
new Date() est de type ISODate().

Par exemple:

obj = { "a" : 1, "b" : ISODate("2012-10"), "c" : NumberLong(42) }

Fonction "insert"

Permet d’insérer un document dans une collection:

db.people.insert([ document ])

Dans le syntaxe db.people.insert():
"db" est le base de données,
"people" est une collection et
"insert()" est une fonction sur la collection.

Une collection comporte toujours un champ "_id" qui est inséré par défaut. La valeur est censé être unique à l’échelle de toutes les bases (espèce de GUID).

Fonction "findOne"

Permet de retourner la 1ère valeur de la collection:

db.people.findOne()

Il est possible de rajouter des arguments:

db.people.findOne( { "name" : "jones" } )

On peut préciser plusieurs conditions dans le premier argument de la fonction:
Par exemple:

db.score.find( { "student" : 19 , "type" : "essay" } );

Permet de remonter tout le document avec des valeurs avec des champs tels que "student" 19 ET les "types" “essay”.

Le 2ème argument de la fonction FIND indique les champs à remonter et les champs à exclure:
Par exemple:

db.score.find( { "student" : 19 , "type" : "essay" }, { "score" : true } );

Dans ce cas on va remonter seulement les champs “score” et "_id" (qui est automatiquement remonté).
Pour exclure le champ "_id", il faut préciser:

db.score.find( { "student" : 19 , "type" : "essay" }, { "score" : true, "_id" : false } );

Fonction "find"

Fonctionne comme la fonction "findOne".
db.people.find() permet d’afficher tout le contenu de la collection.

On peut améliorer l’affichage en écrivant:

db.people.find().pretty()

Opérateurs "$gt" et "$lt"

Permet d’avoir des conditions telles que:
"$gt": strictly greater than
"$lt": strictly less than
"$lte": less or equal than
"$gte": greater or equal than
Par exemple:

db.people.find( { score : { $gt : "95" } } );

Avec plusieurs conditions:

db.people.find( { score : { $gt : "95", "$lt" : 100 }, type : "essay" } );

Utilisation des opérateurs de comparaison sur les chaînes de caractères

Il est possible d’utiliser les opérateurs "$gt" et "$lt" pour les chaînes de caractères:
Par exemple:

db.people.find( { name : { $gt : "B", $lt : "D" } } );

Permet de retourner les noms dont la première lettre est supérieure à "B" et inférieure à "D".
En fait on utilise l’ordre des caractères dans le codage UTF-8 pour savoir moment ordonner les lettres.
Il est possible d’utiliser des types différents dans les champs. Cependant si un champ "name" d’un document est une valeur numérique, il ne sera pas retourné par la requête précédente.

Opérateur $exists

Permet d’indiquer si un champ à l’intérieur d’un document existe ou non.
Par exemple:

db.people.find( { profession : { $exists : true }});

Permet d’indiquer si le champ “profession” existe.
A l’opposé pour chercher les documents où le champ n’existe pas:

db.people.find( { profession : { $exists : false }});

Opérateur $type

Permet de faire un recherche en fonction du type d’un champ.
Par exemple:

db.people.find( { name : { $type : 2}});

Les codes des types sont à retrouver sur http://bsonspec.org/spec.html
Le type 2 correspond au type “string”.

Opérateur $regex

Permet d’indiquer une expression régulière pour faire une recherche.
Par exemple:
db.people.find( { name : { $regex : "a"}}); pour avoir un champ "name" contenant “a”.
db.people.find( { name : { $regex : "e$"}}); pour avoir un champ "name" se terminant avec “e”.
db.people.find( { name : { $regex : "^A"}}); pour avoir un champ "name" commençant par “A”.

Opérateur $or

Opérateur logique:

db.people.find( { $or : [ { name : { $gt : "C" } }, { age : { $exists : true } } ] } );
Remarque:

Faire attention on utilise des crochets pour cet opérateur: $or : [...]

db.scores.find( { $or : [ { score : { $lt : 50 } }, { score : { $gt : 90 } } ] } ) ;

Opérateur $and

Opérateur logique utilisable pour les conditions d’une requête FIND:
Par exemple:

db.people.find( { $and : [ { name : { $gt : "C" } }, { name : { $regex : "a" } } ] } );

Équivalent à:

db.people.find( { name : { $gt : "C" , $regex : "a" } } );
ATTENTION;

db.scores.find( { score : { $gt : 50 }, score : { $lt : 60 } } );

Cette syntaxe est fausse, dans la requète ici, score : { $gt : 50 } est remplacé par score : { $lt : 60 } et la requète exécutée est:

db.scores.find( { score : { $lt : 60 } } );

La syntaxe correcte est:

db.scores.find( { score : { $gt : 50 , $lt : 60 } } );

Recherche à l’intérieur d’un tableau

Si un document est de type:

{ "name" :"George", "favorites": [ "ice cream", "pretzels"]}

Pour chercher à l’intérieur du tableau il suffit de faire:

db.accounts.find( { favorites: "pretzels" })

Ce genre de requêtes ne fonctionne que pour un tableau et seulement pour des valeurs qui se trouvent directement sur le tableau et non dans des tableaux intégrés.

Opérateur $all

Permet d’indiquer que tous les éléments dans un tableau doivent être présents pour que le document soit remonté.
Par exemple:

db.accounts.find( { favorites: { $all : [ "pretzels", "beer" ] } } )

Il faut que le tableau contienne "pretzels" ET "beer", l’ordre n’a pas d’importance.

Opérateur $in

Même chose que l’opérateur $all mais il suffit qu’un seul élément dans le tableau soit présent dans le document pour qu’il soit retourné.
Par exemple:

db.accounts.find( { favorites: { $in : [ "pretzels", "beer" ] } } )

Les documents dont le tableau contient "pretzels" OU "beer" seront remontés.

Notation avec un point "."

Cette notation permet de designer un champ d’un document lorsque celui-ci est composé de document intégré (embedded document).
Un document intégré est de ce type:

name : "richard"
email : { "work" : "richard.lord@work.com",
"personal" : richard.lord@home.com}

Le champ “email” est un document intégré (embedded document).
Si on fait une requête sur ce document, on est obligé d’indiquer tous les champs dans le bon ordre sinon le document contenant se sera pas retourner.
Par exemple:

db.people.find( { email : { "work" : "richard.lord@work.com"} } );

Ne retournera aucun résultat.

db.people.find( { email : { "personal" : "richard.lord@home.com", "work" : "richard.lord@work.com" } } );

Ne retournera aucun résultat.
Pour faire une requête seulement sur le champ "work" il faut utiliser la notation:

db.people.find( { "email.work" : "richard.lord@work.com" } );
ATTENTION:

Il faut utiliser les guillemets "email.work"

Curseur

Permet de parcourir les résultats de la fonction code class=”cs”>”find()”.

cur = db.people.find()

Pour avoir la valeur suivante:

cur.next()

Pour savoir si il existe une valeur suivante:

cur.hasNext()

Ce qui permet de faire une boucle WHILE:

while (cur.hasNext()) printjson(cur.next());

Pour limiter le nombre d’éléments dans le curseur:

cur.limit(5)

Pour ordonner les éléments dans le curseur:
cur.sort( { name: -1 } ); pour afficher dans l’ordre décroissant du champ "name".
Pour sauter 2 documents:

cur.sort( { name: -1 } ).limit(3).skip(2);
ATTENTION:

sort, skip et limit peuvent être utilisé seulement avant que le 1er document soit retourné et avant avoir vérifié que le curseur est vide.

Fonction "count"

code class=”cs”>count() permet de compter le nombre de valeurs:

long count = dbCollection.count();

La syntaxe du Shell pour la fonction "count" est similaire à celle de "find":

db.people.count( { "name" : "john" } );

Fonction "update"

Pour faire des mises à jour.

Par exemple:

dbCollection.update( { name : "Smith" }, { name : "Thompson", salary:50000 })

Cette ligne va remplacer tous les champs du document avec le name ="Smith". Si d’autres champs sont présents, ils seront tous remplacés par le seul champ "salary".
Dans l’exemple: db.people.update( { name : "john" }, { $set : { profession : 1 }} );
Si aucun document ne correspond à "name" = " john", aucune mise à jour ne sera effectuée.

Paramètre $set

Permet de rajouter de nouveaux champs sans effacer les anciens:
Par exemple:

db.people.update( { name : "john" }, { $set : { profession : 1 }} );

Si aucun document ne correspond à "name" = " john", aucune mise à jour ne sera effectuée.

Paramètre $unset

Permet de supprimer un champ d’un document.

La syntaxe est similaire à $set:

db.people.update( { name : "john" }, { $unset : { profession : 1 }} );

Cette ligne va supprimer le champ "profession" du document avec "name" = "john".

ATTENTION

La valeur “1” est ici complêtement arbitraire, elle ne sera pas utilisée par la suite dans le cas du $unset.

Manipulation de tableau

Si on a un tableau d’éléments:

a : [ 1, 2, 3, 4]

Pour modifier la valeur d’un élément du tableau:

db.arrays.update( { _id : 0 }, { $set : { "a.2" : 5 } } )

Permet de modifier la 3e valeur du tableau

a : [ 1, 2, 5, 4]

$push

Permet de rajouter une valeur à la fin du tableau

db.arrays.update( { _id : 0 }, { $push : { a : 6 } } )

Le résultat est:

a : [ 1, 2, 5, 4, 6]

$pop

Permet de supprimer une valeur à gauche avec 1 comme valeur:

db.arrays.update( { _id : 0 }, { $pop : { a : 1 } } )

Le résultat est:

a : [ 1, 2, 5, 4]

Ou supprimer une valeur à droite avec -1:

db.arrays.update( { _id : 0 }, { $pop : { a : -1 } } )

Le résultat est:

a : [ 2, 5, 4]

$pushAll

Permet de rajouter un autre tableau à la suite d’un tableau:

db.arrays.update( { _id : 0 }, { $pushAll : { a : [7, 8, 9] } } )

Le résultat est:

a : [ 2, 5, 4, 7, 8, 9]

$pull

Permet de supprimer une valeur quelque soit sa position dans le tableau

db.arrays.update( { _id : 0 }, { $pull : { a : 5 } } )

Le résultat est:

a : [ 2, 4, 7, 8, 9]

$pullAll

Permet de supprimer une liste de valeurs quelque soit leur position

db.arrays.update( { _id : 0 }, { $pushAll : { a : [2, 4, 8] } } )

Le résultat est:

a : [ 7, 9]

$addToSet

Permet de rajouter un élément dans le tableau s’il n’existe pas déjà. La fonction est idempotente (plusieurs exécutions de la ligne ne rajoutera pas la valeur si elle est déjà présente).

db.arrays.update( { _id : 0 }, { $addToSet : { a : 5 } } )

Le résultat est:

a : [ 7, 9, 5]

Paramètre $upsert

Si la valeur est à false, la commande "update" ne rajoute pas d’entrées si aucun document ne correspond au critère de sélection. Si vraie alors un document est rajoutée même si il n’existe pas à l’origine.

db.people.update( { name : "john" }, { $set : { profession : 1 }, { $upsert : true } } );

Permettra de rajouter le document avec name="john" et profession = 1 même s’il n’existe pas.

Pour utiliser "$upsert" il faut que la valeur du champ soit parfaitement indiquée.

Par exemple:

db.people.update( { age : { $gt : 50 } }, { $set : { name : "john" }, { upsert : true } } );

Cette ligne ne permet pas de déterminer la valeur du champ “age” donc il ne rajoutera pas le champ "age". En revanche, il va rajouter le champ "name".

Paramètre $multi

Si faux, la modification s’effectue seulement sur le premier élément qui correspond aux critères de sélection. Si vraie, alors tous les documents satisfaisant le critère de sélection seront mis à jour.

db.people.update( {}, { $set : { title : "Dr" }, { $multi : true } } );

Permettra de rajouter l’élément "title" à tous les documents de la collection.

ATTENTION

Par défaut "update" ne modifie que la 1ère valeur d’un document satisfaisant le critère. Il faut l’option "$multi" pour la mise à jour se fasse sur plusieurs documents.

Pausing yielding

La mise à jour de plusieurs lignes ne se fait pas de façon atomique. Seul l’écriture des champs est faite de façon atomique. Le pausing yielding consiste, dans le cas où plusieurs opérations de mise à jour sont faites sur les mêmes documents, à permettre la mise à jour de plusieurs documents pour la première opération, puis de plusieurs documents pour la 2ème opération. Ensuite de revenir à la première opération et de permettre la mise à jour d’autres documents et ainsi de suite…

Paramètre $inc

Pour incrémenter une valeur numérique:

db.scores.update( { "score" : { $lt : 70 } }, { $inc : { score : 20} }, { multi : true} )

Permet de rajouter 20 points aux scores inférieurs à 70.

Fonction "remove"

Permet de supprimer des données.
Cette fonction a la même syntaxe que "find".
Contrairement à “update” qui met à jour par défaut seulement le premier document qui correspond aux critères, “remove” supprime tous les documents qui correspondent aux critères.
Par exemple:

db.collection.remove({});

Supprime tous les documents de la table.

db.collection.remove({"id", "alice"});

Permet de supprimer les documents dont le champ "id" est "alice".
La suppression avec "remove" se fait de ligne à ligne contrairement à "drop".
Comme l’update, la suppression de lignes avec "remove" n’est pas atomique.

Fonction "drop"

Permet de supprimer toutes les valeurs d’une collection sans parcourir les lignes une à une.
Plus efficace que "remove" pour supprimer toutes les valeurs.

getLastError

Pour obtenir la dernière erreur survenue:

db.runCommand( { getLastError : 1 } )

Le résultat est un document avec pour élément “err” contenant le message de son erreur.
Cette commande permet aussi de donner un feedback sur certaines commandes sans forcément même si il n’y a pas eu d’erreurs.

Driver .NET

Formation MongoDB M101N: semaine 2 – Driver .NET

Formation MongoDB M101N: semaine 1 – introduction

Quelques caractéristiques:
– “Rich documents”: on peut stocker davantages que de simples valeurs,
– “Prejoin” en utilisant des documents intégrés (embedded documents),
– Pas de jointures en MongoDB,
– Pas de contraintes,
– Pas de transactions,
– Pas de schéma conceptuel.

Le principe général d’une base MongoDB est d’avoir des schémas de données proches de l’application.

Les bases relationnelles sont conçues pour:
– Interdire de modifier la base en introduisant des anomalies,
– Minimise le redesign de la base quand le schéma conceptuel grossit,
– Il permet d’éviter d’être influencer par certains patterns d’accès.

MongoDB ne pousse pas à utiliser un pattern par rapport à un autre. Ensuite il faut éviter d’introduire des anomalies à la création de données.

Exemple d’articles d’un blog

Par exemple, pour afficher les articles d’un blog, on peut stocker tous les éléments de l’article:
– titre,
– auteur,
– contenu,
– date,
– mots clé.

Tous les commentaires:
– auteur du commentaire,
– email,
– contenu du commentaire.

Tous ces éléments peuvent être stockés dans la même collection en utilisant des documents intégrés (embedded document).

Si on utilise un schéma proche de celui qu’on aurait utilisé dans une base relationnelle, on aurait:
– une collection pour les articles avec un ID, titre, corps, auteur et date,
– une collection pour les commentaires avec un ID, un ID d’article (clé étrangère), auteur, adresse email et l’ordre d’affichage du commentaire,
– une collection pour les tags avec un ID, un tag et un ID d’article (clé étrangère).

Ce type de schéma impose de
– maintenir une relation entre les 3 collections en utilisant des clés étrangères,
– maintenir l’ordre dans la collection des commentaires,
– d’utiliser 3 fichiers sur le disque, 1 fichier par collection.
– d’accèder à 3 collections pour afficher un article.

Cette organisation est plus lourde que de considérer une collection pour stocker tous les éléments.

Contraintes

Un des avantages de la base relationnelle est de garantir d’avoir des données consistantes en utilisant des clés étrangères. MongoDB ne garantit pas d’avoir des clés étrangères consistantes.
Le fait d’utiliser des documents intégrés garantit que les documents sont liés entre eux: si tous les commentaires sont directement dans la collection d’articles, par construction ils appartiennent à l’article.

Transactions

Pas de transactions en MongoDB mais les opérations sont atomiques.
Quand on insère des documents dans une collection, ils ne seront visibles que si le document est ajouté entièrement. En revanche, l’ajout de plusieurs documents n’est pas atomiques.
Le fait d’utiliser des documents intégrés permet de garantir la consistance des données.

En utilisant MongoDB, il faut:
– être restrictif en utilisant des documents intégrés ce qui permet d’insérer des données de façon consistante.
– implémenter des “locks” au niveau software
– tolérer une inconsistance dans les données.

Relations entre les objets logiques

Relations un vers un

Par exemple un employé et un CV, un employé n’a qu’un CV.
On peut stocker les infos dans 2 collections:

Employee:
- _ID,
- Name,
- Resume_ID
Resume 
- _ID,
- Jobs: [] (array)
- Education: []

Ou alors on inclut le CV directement dans la collection "Employee" sous forme de document intégré.

Pas de restrictions pour les relations un vers un, il faut juste avoir en tête:
La fréquence d’accès aux données: si on accède très souvent au CV d’un employé ou si on accède seulement aux informations de l’employé.
La taille des éléments: les informations du CV seront peut être plus volumineuse que celles de l’employé lui-même.
La limite de 16mb d’un document intégré: un document intégré ne peut dépasser 16mb.

Relations un vers plusieurs

Par exemple, une ville et une personne. Une ville contient plusieurs personnes.

Plusieurs schémas sont possibles:
1ere solution:

City:
- name,
- area;
- people: [] (array)

Ce schéma n’est pas possible car il y a beaucoup trop de personnes pour une ville, le tableau serait trop grand.

2e solution:

People:
- name,
- city : { name; area; }

Le problème avec ce schéma est de multiplier les données relatives à la ville dans tous les documents des personnes.

3e solution: 2 collections:

People:
- name;
- city: "NYC"
City:
- _ID: "NYC" 
- area

La 3e solution est la meilleure puisqu’on ne duplique pas les informations.

La 1ère solution aurait pu être choisi si il n’y avait pas beaucoup de personnes.

Pour les relations un vers plusieurs, l’important est de savoir l’ordre de grandeur du plusieurs:
dans le cas d’un grand nombre: il faut considérer une collection différente
dans le cas d’un nombre pas très élevé: il est préférable de privilégier un document intégré.

Plusieurs vers plusieurs

Par exemple, des livres et des auteurs.
Dans ce cas, il y a peu de livres pour un auteur et inversement, il a peu d’auteurs pour un livre. Donc il peut considérer 2 collections:

Books:
- _ID, 
- title, 
- authors: [ author_ID ]
Author: 
- _ID 
- Author_name, 
- books: [ books_ID ]

On peut inclure des références vers les auteurs dans la collection livre dans un tableau.

On peut aussi inclure directement les livres dans la collection auteur mais la contrainte est pour les livres qui ont plusieurs auteurs.

Dans le cas d’étudiants et de professeurs:

Un professeur possède beaucoup d’étudiants, on peut utiliser des documents intégrés en incluant un tableau de professeur dans la collection d’étudiants. Ce schéma peut poser problème si des étudiants sont aussi des professeurs.

Index multi-clé

Permet d’utiliser des index pour des relations plusieurs vers plusieurs.

Si on considère 2 collections telles que:

Students: 
- _ID, 
- name, 
- teachers: [ teacher_ID ] (tableau de teacher ID)
Teachers: 
- _ID 
- name,

Si on veut lister les professeurs d’un étudiant, on peut le faire directement. En revanche, si on veut les étudiants d’un professeur, on aura besoin d’un index multi-clé.

db.students.ensureIndex({ 'teachers':1 })

Pour avoir tous les étudiants d’un professeur:

db.students.find({ 'teachers': { $all: [ 0, 1 ] } })

Pour savoir comment l’index est utilisé:

db.students.ensureIndex({ 'teachers':1 }).explain()

Avantages d’utiliser des documents intégrés

– Permet d’améliorer les performances en lecture
– Effectuer un seul aller-retour avec la base de données
La base utilise des disques durs dont le temps d’accès est long mais qui ont une bande passante élevée: on met du temps à attendre l’information mais on peut charger rapidement beaucoup d’information en même temps. Il est donc préférable d’avoir des informations rapprochées pour les charger d’un coup.

Arbres

Par exemple, si on considère des produits et des catégories. Les catégories appartiennent à d’autres categories.
On peut proposer ce schéma:

Products: 
- Category, 
- Product_name
Category: 
- _ID 
- Category_name 
- ancestors: [ category_ID ]

Ainsi tous les ascendants de la categorie se trouvent dans le tableau “ancestors”.

Si on veut les descendants d’un categorie, il suffit de faire:

db.category.find({ ancestors: 35 })

IQueryable vs IEnumerable

Différences entre IQueryable et IEnumerable

Pour effectuer des requêtes Linq, on peut choisir des structures de liste satisfaisant une des deux interfaces IQueryable ou IEnumerable. IQueryable dérive de IEnumerable.

Caractéristiques de IEnumerable:

  • plus appropriée pour la manipulation de listes (listes génériques, Arrays etc…),
  • permet de parcourir une liste dans le sens normal,
  • exécute la requête en base, charge ensuite les données en mémoire puis effectue un filtre sur les données,
  • plus intéressant pour Linq to objects et Linq to XML
  • ne supporte pas les requête personnalisées,
  • ne supporte pas l’initialisation tardive (i.e. Lazy loading) => cette interface n’est pas adaptée pour le “paging”.

Caractéristiques de IQueryable

  • permet de parcourir une liste dans le sens normal (pas de retour en arrière et pas de déplacements entre les éléments),
  • exécute les requêtes dans les serveurs SQL,
  • est appropriée pour exécuter des requêtes sur des bases de données déportées.
  • est plus appropriée pour Linq to SQL,
  • supporte les requêtes personnalisées avec CreateQuery() et Execute(),
  • supporte l’initialisation tardive (Lazy loading) => est adapté au “paging”.

Le garbage collector en .NET en 10 min

Appel au Garbage Collector

Un certain nombre de fonctions permettent d’exécuter le Garbage Collector.
GC.Collect() permet de forcer l’exécution du GC. Un paramètre permet d’indiquer la génération des objets pour lesquelles le GC sera exécutée. La méthode ne certifie pas de l’exécution immédiate du GC, elle permet juste d’indiquer au GC qu’il devra s’exécuter.
GC.GetGeneration(object) permet de connaître la génération d’un objet.
GC.WaitForPendingFinalizers() stoppe le thread en cours jusqu’à ce que toutes les fonctions “finalizers” ont terminés de vider leur file d’attente.
GC.KeepAlive(hr): permet d’indiquer au GC qu’un objet doit être maintenu vivant et ne doit pas être supprimé. La fonction doit être placée à la fin de la partie où on désire qu’il reste vivant:

ObjectType obj = new ObjectType(); 
utilisation de l'objet dans du code..... 
… autre code …. 
GC.KeepAlive(obj);

Cette fonction est utile si on fait appel à du code qui n’est pas lié à des objets managés, à des objets de la WinAPI ou des objets COM.

Object.Finalize()

On ne peut pas faire appel directement à cette fonction. Le GC appelle cette fonction pour libérer les ressources occupées par l’objet. On peut cependant guider l’exécution du GC:
GC.SuppressFinalize() empêchera au GC d’exécuter Finalize().
GC.ReRegisterForFinalize() pour rajouter l’objet dans la liste des objets pour lesquels le GC doit exécuter Finalize().

L’implémentation correspondant à Finalize() dans un objet est le destructeur.

Les objets ayant une implémentation de Finalize() prennent plus de temps pour être détruit. Quand un objet n’est plus accessible par d’autres objets managés, le GC vérifie s’il contient une implémentation de la fonction Finalize():
– s’il ne possède pas une implémentation de Finalize(), l’objet est marqué pour être collecté et sa mémoire est requisitionnée par le GC.
– s’il possède une implémentation de Finalize(), l’objet est marqué pour être finalisé mais il ne sera pas marqué pour être collecté. La méthode Finalize() sera exécutée à la fin de l’exécution du GC. Quand cette méthode sera exécutée, l’objet sera marqué pour être collecté. Au passage suivant du GC, l’objet sera effectivement détruit. Il faut donc 2 passages du GC.

http://msdn.microsoft.com/fr-fr/library/system.object.finalize%28v=vs.110%29.aspx

Génération des objets

Génération 0: objet à courte durée de vie. Le GC s’exécute souvent pour ces objets. Les nouveaux objets sont de génération 0 sauf les objets de grande tailles (qui sont de génération 2). Les objets de génération 0 pour lesquels le GC est appelé seront automatiquement détruits.
Génération 1: c’est un buffer entre les objets de génération 0 et les objets de génération 2 c’est-à-dire entre les objets à courte durée de vie et les objets à longue durée de vie.
Génération 2: ce sont les objets à longue durée de vie. Les objets statiques font parties de cette génération.

Les objets peuvent passer à une génération supérieure (promotion) si ils ont survécu au passage du GC. Les objets qui sont à la génération 2 restent dans cette génération si ils ont survécu au passage du GC. Plus il y aura des objets de génération 2 et plus il sera difficile pour les nouveaux objets d’être de la génération 2. Ainsi on empêche on évite que la génération 2 ne devienne trop grosse et que le GC prenne trop de temps pour s’exécuter.

Exécution du GC

Au demarrage du processus:
Le Garbage Collector alloue 16 ou 32 mo, il va décider de la taille de la génération 0. Ensuite, il donne la main à l’exécution du code.
L’allocation des objets peut se fait jusqu’à ce que la génération 0 soit rempli, ensuite le GC commence son exécution:
Phase de marquage où le GC marque les objets vivants
Réallocation des références des objets pour qu’ils soient compactés
Phase de compactage: le GC récupère l’espace où les objets morts se trouvaient et déplacent les objets qui ont survécus vers un emplacement à la fin du segment de mémoire allouée. Les objets de la génération 2 peuvent être déplacés vers des segments plus anciens. La plupart du temps, les objets de grandes tailles ne sont pas compactés car l’impact sur les performances seront trop importants. Il est possible de forcer le compactage des objets de grandes tailles en faisant appel à:

GCSettings.LargeObjectHeapCompactionMode

Au fur et à mesure, il va créer la génération 2 et déplacer les objets de la génération 1 à la génération 2 etc…

Remarque importante: le GC maîtrise bien la gestion des objets de génération 0 et 1 mais il maîtrise mal les objets qui sont dans la génération 2.

Le GC utilise les éléments suivants pour déterminer si les objets sont vivants ou non:
– Les éléments racines fournis par le JIT (Just-In-Time compiler): les paramètres des méthodes, les membres privés d’une classe, les registres du CPU.
– Les objets managés (GC handles): ces objets sont alloués par l’utilisateur ou par le CLR.
– Les données statiques: les objets statiques dans le domaine d’application pouvant référencés d’autres objets et peuvent permettre à d’autres threads d’être suspendus.

Pendant l’exécution du GC, les autres threads sont suspendus.

Points faibles du GC

– il met en pause l’exécution de tous les threads pour s’exécuter,
– un seul thread fait tout le travail,
– le marquage et le compactage de la génération 2 est long.

Manipulation de ressources non managées

La libération des objets non managées doivent être effectuée explicitement car le GC ne traite que les objets managés.

http://msdn.microsoft.com/en-us/library/ee787088%28v=vs.110%29.aspx

Weak references

Il est possible de créer une weak reference sur un objet de façon à permettre sa suppression dans certaines conditions. Ainsi, pour des objets de grandes tailles qui ne sont pas utilisés réguliérement, il est possible de rajouter une weak reference. Cette reference permettra d’avoir un lien assez faible pour retrouver la valeur de l’objet en cas de besoin et pas trop fort pour qu’en cas de besoin de mémoire le GC puisse le supprimer et réquisitionner la mémoire qu’il occupe.
La condition pour utiliser une weak reference est d’utiliser un objet de grande taille et qui n’est pas utilisé tout le temps.

Il existe 2 types de weak references:
Short: la cible de la référence est nulle si il a été réquisitionné par le GC. C’est la configuration par défaut.
Long: une longue weak reference est retenue lorsque la méthode Finalize de l’objet a été exécutée. L’objet pourra être recréé mais son état n’est pas prévisible. En fait, une fois que la méthode Finalize a été exécutée, l’objet peut être supprimé à tout moment en cas de besoin.


http://msdn.microsoft.com/en-us/library/ms404247%28v=vs.110%29.aspx

http://msdn.microsoft.com/en-us/library/system.weakreference%28v=vs.110%29.aspx

Notifications du GC

On peut recevoir des notifications du GC pour savoir quand il sera exécuté et quand il a terminé son exécution. Ces notifications peuvent être utiles pour des applications manipulant des données de grande taille et où les exécutions du GC peuvent poser problème.

Les fonctions à utiliser sont:
GC.RegisterForFullGCNotification(): pour indiquer que l’on souhaite être notifié. Si on indique un “maxGenerationThreshold” élevé, le seuil de notification sera élevé donc l’exécution du GC se fera plus tardivement et on recevra la notification plus tôt. Si le seuil est trop élevé, il faudra attendre longtemps avant qu’une nouvelle exécution se fasse.
Si le seuil est bas, la probabilité sera élevé que l’exécution du GC se fasse tôt et que la notification soit tardive.
GC.WaitForFullGCApproach(): permet d’indiquer que l’exécution potentiellement bloquante du GC est imminente.
GC.WaitForFullGCComplete(): permet d’indiquer que l’exécution bloquante du GC est terminée.
GC.CancelFullGCNotification(): permet d’indiquer que l’on ne souhaite plus être notifié pour les exécutions du GC.

Toutes ces méthodes doivent être utilisées pour que les notifications se fassent correctement. Il faut de plus vérifier le statut des notifications remontées par “WaitForGCApproach” et “WaitForGCComplete”.

http://msdn.microsoft.com/fr-fr/library/cc713687%28v=vs.110%29.aspx
http://www.abhisheksur.com/2010/08/garbage-collection-notifications-in-net.html

Mode d’exécution du GC

Configuration du GC sur un serveur

L’exécution du GC se fait sur plusieurs threads. Chaque thread s’exécute de façon non-concurrente.

Configuration du GC sur un poste de travail

Il existe qu’un seul thread pour le GC. Plusieurs configurations:
Concurrent: configuration par défaut. Le GC est exécuté toujours sur un thread à part qui marque constamment les objets à supprimer quand l’application est en cours d’exécution. Le GC fait le choix ou non de compacter les données en mémoire en fonction des performances. Le compactage induit la suspension des autres threads mais cette suspension est invisible sur l’interface. Cette configuration permet de meilleurs performances pour les applications avec une GUI.
Non concurrent: le thread du GC est inactif jusqu’à son exécution. Quand l’exécution commence, le GC marque tous les objets, libère la mémoire et la compacte. Les autres threads sont suspendus pendant l’opération. L’application peut ne pas réagir pendant un instant lors de l’exécution du GC.

Il est possible de modifier la configuration dans le fichier de configuration de l’application: le paramètre est gcConcurrent.

Déboguer à distance dans Visual Studio

Pour déboguer à distance, il faut:

  • que les sources soient les mêmes que le code exécutées
  • configurer le parefeu ou le désactiver pour l’accès à la machine hôte à distance
  • que les fichiers PDB soient présents : le répertoire de ces fichiers peut être ajouté dans Tools => Options => Debugging => Symbols
  • que le debugguer à distance Visual soit installé sur la machine hôte (où se trouvent l’exécutable; Visual et le code se trouvant sur la machine cliente).

On peut télécharger le Remote Debugger à l’adresse suivante:
http://www.microsoft.com/fr-fr/download/details.aspx?id=475

Si l’installation ne se passe pas correctement, il suffit de copier les fichiers qui se trouvent sur la machine cliente dans:
C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\Remote Debugger

Authentification

Il faut privilégier une authentification plutôt que l’absence d’authentification. L’authentification Windows nécessite d’avoir un compte administrateur configuré sur la machine hôte avec les mêmes données d’authentification que la machine cliente.

Création d’un compte utilisateur sur la machine hôte

Il faut créer un compte utilisateur sur la machine hôte avec les mêmes données d’authentification que sur la machine cliente pour pouvoir utiliser l’authentification Windows. Pour créer un nouvel utilisateur, il faut:

  • Aller dans Gestion de l’ordinateur: Menu Demarrer => clique droit sur ordinateur => Gérer ou Panneau de configuration => Système et sécurité => Outils d’administration => Gestion de l’ordinateur.
  • Dans Utilisateurs et groupes => Utilisateurs, ajouter un utilisateur avec le même nom que la machine cliente
  • L’utilisateur créé doit faire partie du groupe Administrateur.

Vérifier les éléments de sécurité sur la machine hôte

Aller dans Panneau de configuration => Système et sécurité => Outils d’administration => Stratégie de sécurité locale. Dans la fenêtre, dépliser “Stratégie locale”, puis Options de sécurité. Ensuite, il faut configurer “Accès réseau: modèle de partage et de sécurité pour les comptes locaux” à “Classique – les utilisateurs locaux s’authentifient eux-mêmes”.

Débogage à distance

Remote debugger

Il faut lancer le remote debugger sur la machine hôte avec les droits de l’utilisateur nouvellement créé. Il est préférable de faire le lancement à partir d’une ligne de commande lancée en tant que le nouvel utilisateur en faisant:

runas /user:[nouvel utilisateur] "[Chemin de msvsmon.exe]"

puis configurer l’authentification dans:
Options => Options. Il faut sélectionner “Authentification Windows”.

Visual Studio

Attacher au processus correspondant à l’exécutable sur la machine hôte: Debug => Attach to process. L’exécutable sur la machine hôte doit avoir été lancé en tant que le nouvel utilisateur.
Sélectionner les paramètres suivants:

  • Transport: Default
  • Qualifier [nouvel utilisateur]@[nom machine hôte]
  • puis sélectionner l’application à débuger.

Gestion de la mémoire en .NET en 5 min

Partage de la mémoire en C#

La mémoire utilisée par un processus est divisée en 2 parties: la mémoire privée et la mémoire partagée.
Dans l’environnement .NET, le compilateur JIT compile le code pour chaque assembly séparément. Pour permettre le partage du code, il faut précompiler le code en utilisant NGEN. Ensuite il faut que les processus soient exécutés dans le même AppDomain. Enfin il faut indiquer au compilateur JIT que le code doit être partagé est décorant les fonctions avec l’attribut LoaderOptimization:

[LoaderOptimization(LoaderOptimization.MultiDomain)] 
static void Main(string[] args)

Jitter (JIT)

Le jitter (just-in-time) permet de transformer le code IL des applications en code assembleur. .NET est donc une technologie semi-interprétée. Il effectue cette compilation à la demande quand on fait appel aux assemblies. La compilation est effectuée une seule fois, à la première utilisation.

Points faibles

L’inconvénient majeur de cette technologie est que ça peut poser problème pour les applications qui ont une forte contrainte de démarrage rapide.

Multicore JIT

Depuis le framework 4.5, on va permettre d’optimiser le fonctionnement du JIT en étudiant son fonctionnement et d’enregistrer dans un fichier les décisions du jitter ainsi que la liste des fonctions à compiler. Ainsi au premier démarrage de l’application, le jitter sera optimisé et démarrera plus rapidement en lançant la compilation de certaines assemblies sur tous les processeurs.
Pour effectuer ce traitement, il suffit de rajouter dans le main:

ProfileOptimization.SetProfileRoot(@"C:\Startup"); 
ProfileOptimization.StartProfile("Startup.Profile");

Compiler le code en avance et MPGO

On peut compiler le code en avance en utilisant NGEN.exe. Cependant quand on compile en avance, le code est moins performant que s’il était compilé par le jitter. Ceci s’explique par le fait qu’à l’exécution, le jitter sait quelles sont les assemblies pour lesquelles il faut faire des optimisations puisque qu’il effectue ces modifications à la volée. Le Framework 4.5 permet de répondre à ce problème:
– en étudiant le fonctionnement du jitter pendant une exécution,
– en fournissant à NGEN.exe le résultat de cette étude,
– en compilant en avance en utilisant NGEN.exe.
Le composant qui permet cette optimisation est MPGO: Managed Profile Guided Optimization.
Pour l’utiliser:

Mpgo.exe -scenario Toto.exe -OutDir .\Data -AssemblyList Toto.exe

Pour l’instant l’optimisation faite par MPGO n’est pas au niveau de celle du jitter mais elle va s’améliorer dans les versions futures. L’optimization qui est faite actuellement permet de placer dans les mêmes pages mémoire des fonctions et des données qui s’appellent souvent entre elles (évite de faire appel à des objets dans des pages mémoire différentes qui peuvent éventuellement être swapées etc…).

Gestion des objets pour le Garbage Collector

Suivant leur taille, les objets sont gérés différemment par le Garbage Collector de .NET:
– si l’objet a une taille inférieure à 85000 bytes: il sera placé dans la pile CLR. Les piles de ce type sont compactées de temps en temps, ce qui entraîne Windows a stocké ces objets dans la mémoire physique. Ainsi on intervient directement dans la mémoire physique. Donc si l’application possède beaucoup de petits objets, on va beaucoup intervenir dans la mémoire physique sans utiliser la pile. Il faut donc éviter d’utiliser de petits objets.
– si l’objet a une taille supérieure à 85000 bytes: cet objet est placé dans la pile spécialisée pour les objets larges et il ne sera jamais déplacé par le Garbage Collector. Ainsi on ne va jamais intervenir dans la mémoire physique tant que l’objet ne dépasse pas 2gb pour un processus 32 bits ou que la page soit pleine.

http://geekswithblogs.net/akraus1/archive/2008/11/30/127475.aspx

Pile et tas managé

Les termes anglais sont “Stack” pour pile et “Managed Heap” pour tas managé.

Ces deux structures sont utilisées pour stocker:
pile: les variables locales (stockage par valeur pour les types valeur comme les struct et les énumérations) mais aussi les adresses vers les objets stockés dans le tas managé. Les adresses sont des variables référence.
tas managé: les instances de classe.

Pile (stack)

Un espace dans la pile est alloué lorsqu’on entre dans une méthode.
Les variables sont stockées dans cette espace pendant l’exécution de la méthode. Quand on sort de la méthode, l’espace est supprimé et les variables qui s’y trouvent sont perdues (seules les variables de type valeur et les références vers les instances dans le tas managé sont stockés dans la pile).

Passage de variables dans une fonction

Quand on passe une variable dans une fonction:
les variables de type valeur sont copiés dans un nouvelle espace de la pile. Quand cet espace est supprimé, les variables sont perdues.
pour les variables de type référence, seules les références vers les instances sont copiées. Quand l’espace alloué à la fonction est supprimé, les instances dans le tas managé ne sont pas supprimées.

Tas managé (managed heap)

Il contient les instances des objets de type référence. Comme son nom l’indique, la libération des objets dans cette structure est gérée par le garbage collector.

http://www.c-sharpcorner.com/UploadFile/rmcochran/csharp_memory01122006130034PM/csharp_memory.aspx

Linq en 15 min

Pour qu’on puisse utiliser une source de données avec Linq, il faut que la liste prenne en charge IEnumerable ou IQueryable (IEnumerable se suffit pas, il faut IEnumerable). Linq permet d’effectuer des requêtes sur des types très différents de données. Au moyen de “providers”, on peut effectuer des requêtes sur des fichiers XML avec “Linq to XML”; faire des requêtes sur un SQL server avec “Linq to SQL” ou faire des requêtes sur des entités avec “Linq to Entities”.

Syntaxe

On va s’intéresser à la syntaxe générale de Linq pour les requêtes sur des IEnumerable. Pour que Linq soit utilisable, il faut que la structure de données prenne en charge IEnumerable, si elle prends en charge seulement IEnumerable, va ne sera pas suffisant. La syntaxe générale est:

IEnumerable result = 
       from [identifiant local] in [structure de données] 
       where [condition sur une propriété de l'identifiant] 
       select [propriété de l'identifiant à retourner]

Le résultat des requêtes Linq peuvent être: – IEnumerable, – iEnumerable ou – iQueryable

Exécution

L’exécution des requêtes n’est pas immédiate. Elle sera effectué lorsque la structure du résultat sera utilisée dans une boucle Foreach. Pour forcer l’exécution de la requête, on peut utiliser des fonctions d’agrégation: Count(), Max(), Min() ou Average(). La syntaxe pour ces fonctions est:

var evenNumQuery = 
    from num in numbers 
    where (num % 2) == 0 
    select num; 
int evenNumCount = evenNumQuery.Count();

Si on doit retourner une liste, il faut utiliser: ToList ou ToArray. La syntaxe est alors:

List numQuery2 = 
    (from num in numbers 
     where (num % 2) == 0 
     select num).ToList(); 
// or like this: 
// numQuery3 is still an int[] 
var numQuery3 = 
    (from num in numbers 
     where (num % 2) == 0 
     select num).ToArray();

Clause SELECT

Quelques exemples de clause SELECT: Méthode d’extension Linq exécutée sur une liste:

ContactInfo cInfo = 
     (from ci in app.contactList 
     where ci.ID == id 
     select ci) 
     .FirstOrDefault();

Retour d’un élément de la liste directement:

IEnumerable studentQuery1 = 
     from student in app.students 
     where student.ID > 111 
     select student;

Retour d’un membre d’un élément de la liste:

IEnumerable studentQuery2 = 
     from student in app.students 
     where student.ID > 111 
     select student.Last; 
 
IEnumerable studentQuery4 = 
     from student in app.students 
     where student.ID > 111 
     select student.Scores[0]; 

IEnumerable studentQuery5 = 
     from student in app.students 
     where student.ID > 111 
     select student.Scores[0] * 1.1;

Exécution d’une fonction dans l’élément retourné:

IEnumerable studentQuery3 = 
     from student in app.students 
     where student.ID > 111 
     select student.GetContactInfo(app, student.ID);
 
IEnumerable studentQuery6 = 
     from student in app.students 
     where student.ID > 111 
     select student.Scores.Average();

Retour d’un nouvel élément avec un initialiseur d’objet:

var studentQuery7 = 
     from student in app.students 
     where student.ID > 111 
     select new { student.First, student.Last };
 
IEnumerable studentQuery8 = 
     from student in app.students 
     where student.ID > 111 
     select new ScoreInfo 
     { 
         Average = student.Scores.Average(), 
         ID = student.ID 
     };

Utilisation d’une jointure:

IEnumerable studentQuery9 = 
     from student in app.students 
     where student.Scores.Average() > 85 
     join ci in app.contactList on student.ID equals ci.ID 
     select ci;

Lien MSDN

Clause FROM

Il est possible d’utiliser des “From” imbriqués qui correspondent à 2 boucles “foreach”: une première boucle pour la liste d’élément et une 2ème pour une liste dans la première liste:

var scoreQuery = 
     from student in students 
     from score in student.Scores 
     where score > 90 
     select new { Last = student.LastName, score };

Le “select” permet de retourner des éléments parmi ceux manipulés dans les 2 from. On peut faire des clauses from imbriquées pour des listes qui n’ont pas de lien entres elles:

char[] upperCase = { 'A', 'B', 'C' }; 
char[] lowerCase = { 'x', 'y', 'z' }; 
 
var joinQuery1 = 
     from upper in upperCase 
     from lower in lowerCase 
     select new { upper, lower }; 
 
var joinQuery2 = 
     from lower in lowerCase 
     where lower != 'x' 
     from upper in upperCase 
     select new { lower, upper };

Clause WHERE

Opérateurs logiques

Dans une clause WHERE, on peut spécifier plusieurs conditions séparées par des opérateurs logiques ET et OU mais ATTENTION il faut utiliser: – “&&” pour ET – “||” pour OU Par exemple:

var queryLondonCustomers =  
     from cust in customers  
     where cust.City=="London" && cust.Name == "Devon" 
     select cust;

Utilisation de fonctions

On peut utiliser une fonction dans la clause WHERE:

var queryEvenNums =  
     from num in numbers  
     where IsEven(num)  
     select num;

Lien MSDN

Clause JOIN

Opérateur de comparaison “EQUALS”

L’opérateur de comparaison utilisé pour les jointures est seulement “EQUALS”. On ne peut pas utiliser “supérieur à” ou “différent de” ce qui limite beaucoup fonctionnellement l’intérêt de linq.

Jointure interne

Seuls les éléments communs aux 2 listes sont retournés:

var innerJoinQuery = 
     from category in categories 
     join prod in products on category.ID equals prod.CategoryID 
     select new { ProductName = prod.Name, Category = category.Name };

Jointure avec une clé composite:

On peut appliquer l’opérateur d’égalité sur une structure plus complexe:

IEnumerable query =  
     from employee in employees  
     join student in students on new { employee.FirstName, employee.LastName } 
     equals new { student.FirstName, student.LastName }  
     select employee.FirstName + " " + employee.LastName;

Jointure multiple

Comme pour les autres opérateurs, on peut utiliser plusieurs clause JOIN pour effectuer une jointure multiple:

var query =  
     from person in people  
     join cat in cats on person equals cat.Owner  
     join dog in dogs on new { Owner = person, Letter = cat.Name.Substring(0, 1)} 
     equals new { dog.Owner, Letter = dog.Name.Substring(0, 1) }  
     select new { CatName = cat.Name, DogName = dog.Name };

Jointure groupée

Ce type de jointure n’a pas d’équivalent en requête relationnelle, elle permet d’identifier le résultat d’une jointure dans une variable pour être facilement utilisable dans la clause SÉLECT ou dans une nouvelle requête:

var innerGroupJoinQuery = 
    from category in categories 
    join prod in products on category.ID equals prod.CategoryID into prodGroup 
    select new { CategoryName = category.Name, Products = prodGroup };

Pour une nouvelle requête:

var innerGroupJoinQuery2 = 
    from category in categories 
    join prod in products on category.ID equals prod.CategoryID into prodGroup 
    from prod2 in prodGroup 
    where prod2.UnitPrice > 2.50M 
    select prod2;

Jointure externe gauche

On retourne les éléments de la source de gauche même s’ils n’existent pas dans la source de droite. Il faut utiliser la méthode “DefaultIfEmpty” avec une jointure groupée pour indiquer les valeurs à retourner si la source de droite ne contient pas de valeurs.

var leftOuterJoinQuery = 
    from category in categories 
    join prod in products on category.ID equals prod.CategoryID into prodGroup 
    from item in prodGroup.DefaultIfEmpty(new Product { Name = String.Empty, CategoryID = 0 }) 
    select new { CatName = category.Name, ProdName = item.Name };

Lien MSDN

Non equi-jointure

Une jointure n’est possible qu’avec un opérateur d’égalité. Pour utiliser un autre opérateur, il existe quelques astuces en utilisant 2 clauses FROM:

var nonEquijoinQuery = 
     from p in products 
     let catIds = from c in categories 
               select c.ID 
     where catIds.Contains(p.CategoryID) == true 
     select new { Product = p.Name, CategoryID = p.CategoryID };

Lien MSDN

Mot clé ORDERBY

Permet d’ordonner les résultats dans la structure IEnumerable. On utilise: – “ascending” pour l’ordre croissant, – “descending” pour l’ordre décroissant.

IEnumerable sortDescendingQuery =  
     from w in fruits  
     orderby w descending  
     select w;

On peut ordonner suivant plusieurs éléments:

IEnumerable sortedStudents =  
from student in students  
orderby student.Last ascending, student.First ascending  
select student;

Lien MSDN

Clause GROUP…BY

“Group…by” permet de grouper les éléments mais attention la structure de sortie n’est plus une IEnumerable mais IEnumerable<IGrouping>. TGROUPING est le type ayant permis de grouper les éléments. IGrouping est comme un dictionnaire. ATTENTION: ce mot clé s’utilise avec la syntaxe:

GROUP [identifiant local] BY [élément utilisé pour grouper]

Il n’est pas nécessaire d’utiliser une clause SELECT lorsqu’on utilise le mot clé GROUP…BY:

var queryCustomersByCity =  
     from cust in customers  
     group cust by cust.City;

Utilisation avec INTO:

Into permet d’utiliser une variable qui sera utilisée ensuite dans une autre clause WHERE ou SELECT:

var custQuery =  
     from cust in customers  
     group cust by cust.City into custGroup  
     where custGroup.Count() > 2  
     orderby custGroup.Key  
     select custGroup;

Lien MSDN

Mot clé LET

LET permet de définir une variable qui pourra être utilisée dans une 2e clause FROM, dans une clause SELECT ou dans une clause WHERE:

var earlyBirdQuery =  
     from sentence in strings 
     let words = sentence.Split(' ') 
     from word in words 
     let w = word.ToLower() 
     where w[0] == 'a' || w[0] == 'e' || w[0] == 'i' || w[0] == 'o' || w[0] == 'u' 
     select word;

Lien MSDN