LINQ et le nouveau modèle de données de SharePoint 2010

LefortLudovic

Note : for the English version, click here

L'objectif de cet article est d'analyser les principaux changements apportés par SharePoint 2010 dans son modèle de données. Une API LINQ est à présent disponible en standard. Nous allons donc voir comment manipuler le successeur des requêtes CAML et nous nous poserons la question suivante : CAML est-il mort ou a-t-il encore de beaux jours devant lui?.

- Titre : LINQ et le nouveau modèle de données de SharePoint 2010

- Auteur : LefortLudovic

- Parution : 28 novembre 2009

- Licence : Copyright ®  Developpez. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Nouveau modèle relationnel

- II-A. Le modèle en image

- II-B. Les changements dans l'API liés à ce nouveau modèle

- II-B-1. Enumerer les colonnes liées d'une liste

- II-B-2. Ajouter une colonne liée

- II-B-3. Gérer l'unicité d'une colonne

- III. LINQ et l'acquisition de données

- III-A. Acquisition d'un contexte LINQ

- III-A-1. SPMetal

- III-A-2. Exploiter la classe générée par SPMetal

- III-B. Requêtes simples

- III-C. Requêtes efficaces et inefficaces

- III-C-1. Exemples de requêtes inefficaces

- III-D. Effectuer des jointures

- III-D-1. Alternatives aux jointures LINQ

- III-D-1-a. Jointure simple en CAML

- III-D-1-b. Jointure multiple en CAML

- IV. LINQ et la mise à jour de données

- IV-A. Insertion d'éléments

- IV-B. Mise à jour d'un élément

- IV-C. Suppression d'un élément

- IV-D. Mise à jour massive

- IV-E. Gestion des conflits de mise à jour

- IV-E-1. Détection des conflits

- IV-E-3. Résolution des conflits - Confirmation de mise à jour

- IV-E-4. Résolution des conflits - Confirmation/Annulation des mises à jour

- V. Et le CAML dans tout cela?

- V-A. Compatibilité arrière

- V-B. Faut-il encore utiliser les requêtes CAML?

- VI. Téléchargement

I - Introduction

SharePoint 2010 étant disponible en version Beta, vous pouvez désormais appréhender les nouveautés dont cette version regorge. Quelques statistiques ci et là démontrent que SharePoint 2010 correspond à une augmentation des API de plus ou moins 150%. Nous avons donc tous du pain sur la planche :).

Cet article se concentre sur l'une des grandes nouveautés liée à l'accès aux données qui était précédemment presque systématiquement réalisée via des requêtes CAML. Ces mêmes requêtes qui ont toujours eu tendance à décourager les développeurs dotnet "classiques" lors de leur apprentissage de SharePoint par leur singularité, complexité et limitations.

A la fin de cet article, vous pourrez télécharger un exemple complet de programme permettant d'effectuer toute une série de tests de manipulations de données via LINQ. Des requêtes utilisant ou non des jointures, des mises à jour (update/insert/delete) d'éléments de liste et la gestion de conflits liés aux mises à jour sont démontrées dans ce programme et tout au long de l'article. Voici une capture d'écran illustrant le programme :

Il vous suffit de double-cliquer sur une option et le programme exécutera la méthode correspondante de manière interactive ou non. Ces différentes méthodes ainsi que des explications sont fournies dans cet article. Vous pourrez par ailleurs télécharger un modèle de site (.wsp) contenant les listes et les données sur lesquelles se base le programme.

Pour les différents exemples, trois listes sont utilisées. Il s'agit des listes Customers, Cities et Orders. La liste Customers contient une colonne lookup qui pointe vers Cities et pour laquelle l'intégrité référentielle est appliquée. La liste Orders contient une colonne lookup qui pointe vers Customers.

II - Nouveau modèle relationnel

Il a toujours été reproché à SharePoint 2007 de ne pas offrir de modèle relationnel et d'intégrité référentielle entre les différentes listes d'un site. Ce type d'intégrité devait être prise en charge par des Event Handlers spécialement développés. Dans le même ordre d'idées, SharePoint n'offrait pas la possibilité de créer des colonnes uniques. Seules les colonnes de type Lookup représentaient des relations "logiques" entre les différences listes mais ni l'unicité ni l'intégrité des données n'étaient assurées nativement.

Cette "lacune" est à présent comblée par SharePoint 2010 qui offrent d'emblée la possibilité de créer de réelles relations entre les listes et d'appliquer des critères d'unicité sur les colonnes.

II-A - Le modèle en image

Lorsque l'on établit une relation entre deux listes via une colonne de type recherche, il est à présent possible de forcer l'intégrité référentielle, comme vous illustré ci-dessous :

Il est possible de choisir entre deux options :

- Restrict Delete : empêche la suppression d'un élément de la liste source si celui-ci est référencé dans la liste cible

- Cascade Delete : force la suppression des éléments de la liste cible référençant un élément de la liste source

En plus de cette intégrité, il est à présent possible de définir si une colonne doit être unique :

II-B - Les changements dans l'API liés à ce nouveau modèle

Ces changements ont bien sûr provoquer des modifications dans l'API avec l'apparition de nouveaux objets.

II-B-1 - Enumerer les colonnes liées d'une liste

Ce code :

using (SPSite TargetSite = new SPSite(TargetSiteUrl))
{
using (SPWeb TargetWeb = TargetSite.OpenWeb())
{

SPRelatedFieldCollection RelatedFields = TargetWeb.Lists["Cities"].GetRelatedFields();
foreach (SPRelatedField RelatedField in RelatedFields)
{
Console.WriteLine("Field <{0}>\r\nbound to <{1}>\r\nlookup on <{2}>\r\nbehavior <{3}>\r\nWeb <{4}>",
RelatedField.FieldId.ToString(),
TargetWeb.Lists[RelatedField.ListId].Title,
RelatedField.LookupList,
RelatedField.RelationshipDeleteBehavior,
RelatedField.WebId);
}
}
}

permet de lister toutes les colonnes liées vers ou depuis la liste Cities. En l'occurrence dans l'exemple, ça retourne ceci :

c'est à dire la relation entre Cities et Customers à laquelle l'intégrité référentielle est appliquée et est de type Restrict. Ce type de relation empêche donc une ville d'être supprimée si celle-ci est référençée par un ou plusieurs clients.

Les objets SPRelatedField et SPRelatedFieldCollection nous permettent donc de gérer ces relations.

II-B-2 - Ajouter une colonne liée

Voici un exemple de code qui crée les éléments suivants :

1. Une liste de données source

2. Une liste de données cible

3. Une colonne de type lookup pointant depuis la liste cible vers la liste source

4. Le programme applique l'intégrité référentielle

5. Un élément source est créé

6. Un élément cible est créé avec une donnée dans le champ lookup

7. Le programme tente de supprimer l'élément source => le programme génère une exception car l'intégrité a fonctionné.

static void CreateRelationShip()
{
string NewLookupFieldTitle = "New Field";
using (SPSite Site = new SPSite("http://win-a8m4b9i5u4w/test"))
{
using (SPWeb Web = Site.OpenWeb())
{
try
{

SPListCollection Lists = Web.Lists;

Guid SourceListId = Lists.Add("Source List",
"",
SPListTemplateType.GenericList);

Console.WriteLine("Source List Created");

Guid TargetListId = Lists.Add("Target List",
"",
SPListTemplateType.GenericList);

Console.WriteLine("Target List Created");

SPList SourceList = Lists[SourceListId];
SPList TargetList = Lists[TargetListId];
SPFieldCollection Fields = TargetList.Fields;
Fields.AddLookup(NewLookupFieldTitle, SourceList.ID, true);
Console.WriteLine("Lookup Field Created");
SPFieldLookup NewLookupField = Fields[NewLookupFieldTitle] as SPFieldLookup;
NewLookupField.Indexed = true;
NewLookupField.LookupField = "Title";
NewLookupField.RelationshipDeleteBehavior = SPRelationshipDeleteBehavior.Restrict;
NewLookupField.Update();
Console.WriteLine("Lookup Field Integrity enfored");
SPListItem NewSourceItem = SourceList.Items.Add();
NewSourceItem["Title"] = "Source Data";
NewSourceItem.Update();
Console.WriteLine("Source List Item Created");
SPListItem NewTargetItem = TargetList.Items.Add();
NewTargetItem["Title"] = "Target Data";
NewTargetItem[NewLookupFieldTitle] = new SPFieldLookupValue(1, "Source Data");
NewTargetItem.Update();
Console.WriteLine("Source List Item Created");
TargetList.Update();
SourceList.Update();
Console.WriteLine("Trying to delete the referenced source item...");
NewSourceItem.Delete();
}
catch (SPException Ex)
{
Console.WriteLine(Ex.Message);
}

}
}
}

Le résultat est celui-ci :

mais l'on constate qu'il a bien créé deux listes avec un élément dans chaque :

II-B-3 - Gérer l'unicité d'une colonne

Maintenant que les colonnes peuvent être uniques, regardons de plus près les propriétés impliquées. Pour spécifier qu'une colonne doit être unique, vous pouvez utiliser le code suivant :

using (SPSite TargetSite = new SPSite(TargetSiteUrl))
{
using (SPWeb TargetWeb = TargetSite.OpenWeb())
{

SPRelatedFieldCollection RelatedFields = TargetWeb.Lists["Cities"].GetRelatedFields();
SPField TheField = TargetWeb.Lists["Customers"].Fields.GetFieldByInternalName("Title");
TheField.Indexed = true;
TheField.AllowDuplicateValues = false;
TheField.Update();

}
}

Il faut d'abord spécifier que la colonne est indexée et ensuite positionner sa propriété AllowDuplicateValues à false.

III - LINQ et l'acquisition de données

En préambule, il est utile de signaler que l'API LINQ SharePoint exécute toujours du code CAML en interne pour exécuter des requêtes. Tout cela peut se faire grâce à l'instantication d'un objet de type DataContext qui a été généré par SPMETAL.

III-A - Acquisition d'un contexte LINQ

III-A-1 - SPMetal

SPMetal est une nouvelle ligne de commande délivrée dans le 14\BIN permettant de générer une classe de type proxy que l'on utilisera comme contexte LINQ. Il suffit d' exécuter cette commande sur un site existant pour récupérer une classe dotnet qui crée toutes les entités requises pour manipuler les données en lecture/écriture.

Voici par exemple comment créer une telle classe:

spmetal /web:<url> /namespace:<namespace> /code:<codefile.cs>

Essayons d'analyser une partie du code généré par SPMetal. Il est important de comprendre ce qu'il fait dans la mesure où vous pourrez être amené à modifier ce fichier si l'une ou l'autre structure n'est reprise automatiquement par ce dernier.

Prenons l'exemple de l'entité CustomersItem représentant un élément de la liste Customers. L'élément CustomersItem contient différentes métadonnées telles que Title, Age, City qui est une colonne lookup pointant vers les villes et enfin une colonne OrdersItem qui est une référence de la liste Orders vers la liste Customers.

Le code généré par SPMetal pour l'entité CustomersItem est donc:

[Microsoft.SharePoint.Linq.ListAttribute(Name="Customers")]
public Microsoft.SharePoint.Linq.EntityList<CustomersItem> Customers
{
get
{
return this.GetList<CustomersItem>("Customers");
}
}

ceci représente une propriété retournant la liste des Customers que l'on peut réutiliser dans les objets utilisant le datacontext.

Le code représentant l'entité elle-même est le suivant:

[Microsoft.SharePoint.Linq.ContentTypeAttribute(Name="Item", Id="0x01", List="Customers")]
public partial class CustomersItem : Item {
private System.Nullable<double> _age;
private Microsoft.SharePoint.Linq.EntityRef<CitiesItem> _city;
private Microsoft.SharePoint.Linq.EntitySet<OrdersItem> _ordersItem;

#region Extensibility Method Definitions
partial void OnLoaded();
partial void OnValidate();
partial void OnCreated();
#endregion

public CustomersItem() {
this._city = new Microsoft.SharePoint.Linq.EntityRef<CitiesItem>();
this._city.OnSync += new System.EventHandler<Microsoft.SharePoint.Linq.AssociationChangedEventArgs<CitiesItem>>(this.OnCitySync);
this._city.OnChanged += new System.EventHandler(this.OnCityChanged);
this._city.OnChanging += new System.EventHandler(this.OnCityChanging);
this._ordersItem = new Microsoft.SharePoint.Linq.EntitySet<OrdersItem>();
this._ordersItem.OnSync += new System.EventHandler<Microsoft.SharePoint.Linq.AssociationChangedEventArgs<OrdersItem>>(this.OnOrdersItemSync);
this._ordersItem.OnChanged += new System.EventHandler(this.OnOrdersItemChanged);
this._ordersItem.OnChanging += new System.EventHandler(this.OnOrdersItemChanging);
this.OnCreated();
}
[Microsoft.SharePoint.Linq.ColumnAttribute(Name = "Age", Storage = "_age", FieldType = "Number")]
public System.Nullable<double> Age
{
get
{
return this._age;
}
set
{
if ((value != this._age))
{
this.OnPropertyChanging("Age", this._age);
this._age = value;
this.OnPropertyChanged("Age");
}
}
}
[Microsoft.SharePoint.Linq.AssociationAttribute(Name="City", Storage="_city", MultivalueType=Microsoft.SharePoint.Linq.AssociationType.Single, List="Cities")]
public CitiesItem City {
get {
return this._city.GetEntity();
}
set {
this._city.SetEntity(value);
}
}
[Microsoft.SharePoint.Linq.AssociationAttribute(Name = "Customer", Storage = "_ordersItem", MultivalueType = Microsoft.SharePoint.Linq.AssociationType.Backward, List = "Orders")]
public Microsoft.SharePoint.Linq.EntitySet<OrdersItem> OrdersItem
{
get
{
return this._ordersItem;
}
set
{
this._ordersItem.Assign(value);
}
}

private void OnCityChanging(object sender, System.EventArgs e) {
this.OnPropertyChanging("City", this._city.Clone());
}

private void OnCityChanged(object sender, System.EventArgs e) {
this.OnPropertyChanged("City");
}

private void OnCitySync(object sender, Microsoft.SharePoint.Linq.AssociationChangedEventArgs<CitiesItem> e) {
if ((Microsoft.SharePoint.Linq.AssociationChangedState.Added == e.State)) {
e.Item.CustomersItem.Add(this);
}
else {
e.Item.CustomersItem.Remove(this);
}
}
private void OnOrdersItemChanging(object sender, System.EventArgs e)
{
this.OnPropertyChanging("OrdersItem", this._ordersItem.Clone());
}
private void OnOrdersItemChanged(object sender, System.EventArgs e)
{
this.OnPropertyChanged("OrdersItem");
}
private void OnOrdersItemSync(object sender, Microsoft.SharePoint.Linq.AssociationChangedEventArgs<OrdersItem> e)
{
if ((Microsoft.SharePoint.Linq.AssociationChangedState.Added == e.State))
{
e.Item.Customer = this;
}
else
{
e.Item.Customer = null;
}
}
}

Soit principalement des getters et setters permettant de manipuler les métadonnées. Il est intéressant de noter que l'implémentation des colonnes City et OrdersItem diffèrent au niveau de leur setteur. Celui de City appelle la méthode SetEntity() alors que l'autre appelle la méthode Assign().

Pour limiter le listing du code, je n'ai pas inclus la représentation des autres entités (vous pourrez toutefois vous y référrer si vous téléchargez la solution.

III-A-2 - Exploiter la classe générée par SPMetal

Lorsqu'un contexte a été généré via SPMetal, vous pouvez l'ajouter à votre projet et directement commencer à l'utiliser. Si via SPMetal, vous avez généré la classe TeamSite, vous pouvez acquérir un contexte comme ceci :

TeamSiteDataContext Ctx = new TeamSiteDataContext("http://urldusite");

ou encore

TeamSiteDataContext Ctx = new TeamSiteDataContext(SPContext.Current.Web.Url);

si le contexte courant n'est pas nul.

III-B - Requêtes simples

Comme précisé dans l'introduction, tous les exemples sont basés sur un site contenant trois listes. Voici une capture d'écran des listes en question :

Avec LINQ, effectuer une requête sur une liste données relève d'un jeu d'enfant, voyez plutôt :

EntityList<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers");
var CustomerItems = from Customer in Customers
select Customer;

foreach (var CustomerItem in CustomerItems)
{
Console.WriteLine(string.Format("Customer <{0}> aged <{1}> lives in <{2}> - <{3}>",
(CustomerItem.Title != null) ? CustomerItem.Title : "",
(CustomerItem.Age != null) ? CustomerItem.Age.ToString() : "-",
(CustomerItem.City != null && CustomerItem.City.Title != null) ? CustomerItem.City.Title : "",
(CustomerItem.City != null && CustomerItem.City.Country != null) ? CustomerItem.City.Country : "")
);
}

En admettant que la variable Ctx représentant le contexte tel qu'expliqué dans la section précédente ait déjà été instanciée, Cette requête de deux lignes nous permet d'afficher des données provenant de différentes listes, en l'occurence, les listes Customers et Cities. Il faut instancier une entité de type CustomersItem, la faire pointer sur la liste Customers et ensuite préparer la requête.

En effet, le rendu affiche à la fois des informations liées au client mais également le pays de la ville où celui-ci habite. Voici en image, le résultat de la requête précédente :

Dans le même ordre d'idée, on peut compliquer un peu la requête en spécifiant quelques critères supplémentaires comme par exemple :

EntityList<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers");
var CustomerItems = from Customer in Customers
where Customer.City.Title == "Los Angeles" && Customer.Age > 30
select Customer;

foreach (var CustomerItem in CustomerItems)
{
Console.WriteLine(string.Format("Customer <{0}> aged <{1}> lives in <{2}> - <{3}>",
(CustomerItem.Title != null) ? CustomerItem.Title : "",
(CustomerItem.Age != null) ? CustomerItem.Age.ToString() : "-",
(CustomerItem.City != null && CustomerItem.City.Title != null) ? CustomerItem.City.Title : "",
(CustomerItem.City != null && CustomerItem.City.Country != null) ? CustomerItem.City.Country : "")
);
}

Ce qui aura pour effet de ne retourner que les clients habitant Los Angeles et étant agé de plus de 30 ans. Si vous ne disposez que de l'ID de la ville, il vous suffit de remplacer Customer.City.Title par Customer.City.Id...Vous voyez que les possibilités sont grandes et seulement en quelques lignes de code.

III-C - Requêtes efficaces et inefficaces

L'API LINQ génère du code CAML pour retourner les résultats souhaités. Toute requête LINQ pouvant être "traduite" directement en CAML est considérée comme efficace. Toute requête nécessitant une manipulation supplémentaire sur les données est considérée comme inefficace.

Avant la beta2, il était possible de positionner la propriété AllowInefficientQueries à true/false pour autoriser ou non les requêtes inefficaces. SPLINQ se chargeait alors d'effectuer les manipulations supplémentaires comme par exemple des opérations de groupe, d'aggréation etc...lui-même. Depuis la beta2 (on verra à l'avenir), la propriété AllowInefficientQueries est positonnée à false et n'est plus modifiable, ce qui rend les requêtes inefficaces non supportées.

La liste des opérations non efficaces se trouve sur MSDN, http://msdn.microsoft.com/en-us/library/ee536585(office.14).aspx

Vous devez donc effectuer le travail additionnel vous-même si vous souhaitez exécuter ce genre de requêtes. Les exemples de requêtes précédents étaient tous considérés comme Efficaces dans la mesure où ils n'utilisaient aucun opérateur ne pouvant être traduit directement en CAML.

A chaque fois que votre code tentera d'exécuter une requête inefficace, vous recevrez le message d'erreur suivant:

qui sème la confusion car il précise que vous devez positionner AllowInefficientQueries à True alors que cette propriété n'est plus mise à disposition. Ceci sera corrigé dans la prochiane version de SharePoint, ne perdez donc pas votre temps à essayer de la retrouver :).

III-C-1 - Exemples de requêtes inefficaces

Opération de groupe sur les villes et les clients. Cette requête est censée retourner le nombre de clients de moins de 35 ans par ville:

IEnumerable<IGrouping<CitiesItem, CustomersItem>> CustomersByCity = Ctx.Customers.Where(c => c.Age < 35).GroupBy(c => c.City);
foreach (var CustomerCity in CustomersByCity)
{
CitiesItem CityGroup = CustomerCity.Key as CitiesItem;
Console.WriteLine(string.Format("Number of customers aged < 35 living in {0} is {1}",
CityGroup.Title, CustomerCity.Count()));
}

Cette requête est considérée comme inefficace car elle demande à SPLINQ de grouper les éléments sur la ville. Pour pouvoir utiliser ce type de requête, vous devez effectuer l'opération de groupe en passant par LINQ to Objects comme suit:

IEnumerable<IGrouping<CitiesItem, CustomersItem>> CustomersByCity = Ctx.Customers.Where(c => c.Age < 35).ToList().GroupBy(c => c.City);
foreach (var CustomerCity in CustomersByCity)
{
CitiesItem CityGroup = CustomerCity.Key as CitiesItem;
Console.WriteLine(string.Format("Number of customers aged < 35 living in {0} is {1}",
CityGroup.Title, CustomerCity.Count()));
}

L'appel à la méthode .ToList() fait en sorte que l'opération de groupe n'est pas exécutée par SPLINQ mais par LINQ to Objects. Du coup, SPLINQ ne fait que retourner la liste des clients de moins de 35 ans, ce qui est bien sûr considéré comme efficace.

Autre exemple de requête inefficace, c'est lorsque vous tentez d'effectuer plusieurs jointures:

EntityList<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers");
EntityList<CitiesItem> Cities = Ctx.GetList<CitiesItem>("Cities");
EntityList<OrdersItem> Orders = Ctx.GetList<OrdersItem>("Orders");

var QueryResults = from Customer in Customers
join City in Cities on Customer.City.Id equals City.Id
join OrderItem in Orders on Customer.Id equals OrderItem.Customer.Id
select new { CityName = City.Title, City.Country, Customer.Title, OrderTitle = OrderItem.Title };

selon le même principe que précédemment, vous devrez transiter par LINQ to Objects pour effectuer des jointures multiples. Voici l'équivalent de l'exemple ci-dessus en passant par LINQ to objects:

List<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers").Where(c=>c.City!=null).ToList();
List<CitiesItem> Cities = Ctx.GetList<CitiesItem>("Cities").ToList();
List<OrdersItem> Orders = Ctx.GetList<OrdersItem>("Orders").Where(o => o.Customer != null).ToList();

var QueryResults = from Customer in Customers
join City in Cities on Customer.City.Id equals City.Id
join OrderItem in Orders on Customer.Id equals OrderItem.Customer.Id
select new { CityName = City.Title, City.Country, Customer.Title, OrderTitle = OrderItem.Title };

A nouveau, on appelle la méthode ToList() pour récupérer des listes d'objets sur lesquels on effectue une jointure par la suite. On s'assure également de ne rappatrier que les clients référençant une ville et les commandes référençant un client. Ceci n'est pas obligatoire mais cela permet d'éviter de rappatrier des données inutiles.

III-D - Effectuer des jointures

Seules les jointures implicites sont supportées par SPLINQ Beta 2. Les jointures multiples sont considérées comme inefficaces. Voici un exemple de jointure toléré si l'on considère qu'une colonne Lookup a été créée entre les listes Customers et Cities:

EntityList<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers");
EntityList<CitiesItem> Cities = Ctx.GetList<CitiesItem>("Cities");

var QueryResults = from Customer in Customers
join City in Cities on Customer.City.Id equals City.Id
select new { CityName=City.Title, City.Country, Customer.Title };

var Results = QueryResults.ToList();
if (Results.Count > 0)
{
Results.ForEach(cc => Console.WriteLine("Customer <{0}> lives in <{1}> - <{2}>",
(cc.Title != null) ? cc.Title : "-",
(cc.CityName != null) ? cc.CityName : "-",
(cc.Country != null) ? cc.Country : "-"));
}
else
{
Console.WriteLine("No data found!");
}

La jointure est symbolisée par le mot clé join qui permet de lier les entités entre-elles. Vous remarquerez que dans la création de notre objet anonyme, on utilise CityName comme alias. Ceci est obligatoire car l'entité CustomersItem et l'entité CitiesItem déclarent toutes deux un membre nommé Title. Il n'est pas autorisé de déclarer deux fois le même nom de membre dans un objet anonyme, d'où l'utilisation de l'alias. Le résultat de la requête précédente donne donc ceci :

C'est à dire la liste de tous les clients habitant une ville dont l'ID est bien trouvé dans la liste des villes.

III-D-1 - Alternatives aux jointures LINQ

Comme on l'a vu dans la section précédente, les 3/4 des jointures sont considéres comme inefficaces. Donc, vous devrez soit passer par LINQ to Objects comme expliqué précédemment, soit éventuellement par des classes intermédaires qui pour certains cas génèreront le CAML elles-mêmes.

Voici un exemple de jointure multiple en CAML car l'une des évolutions majeures du CAML est précisémment qu'il est possible d'effectuer des jointures

III-D-1-a - Jointure simple en CAML

SPList CustomerList = Web.Lists["Customers"];
SPQuery CustomerCityQuery = new SPQuery();
CustomerCityQuery.Joins =
"<Join Type='INNER' ListAlias='Cities'>" +
"<Eq>" +
"<FieldRef Name='City' RefType='Id' />" +
"<FieldRef List='Cities' Name='ID' />" +
"</Eq>" +
"</Join>";
StringBuilder ProjectedFields = new StringBuilder();
ProjectedFields.Append("<Field Name='CityTitle' Type='Lookup' List='Cities' ShowField='Title' />");
ProjectedFields.Append("<Field Name='CityCountry' Type='Lookup' List='Cities' ShowField='Country' />");
CustomerCityQuery.ProjectedFields = ProjectedFields.ToString();
SPListItemCollection Results = CustomerList.GetItems(CustomerCityQuery);
foreach (SPListItem Result in Results)
{
SPFieldLookupValue CityTitle = new SPFieldLookupValue(Result["CityTitle"].ToString());
SPFieldLookupValue CityCountry = new SPFieldLookupValue(Result["CityCountry"].ToString());

Console.WriteLine(string.Format("Customer {0} lives in {1} - {2}",
Result.Title,
CityTitle.LookupValue,
CityCountry.LookupValue));
}

L'objet SPQuery est à présent doté de la propriété Joins qui permet d'assigner une ou plusieurs jointures. Afin de rappatrier les valeurs des colonnes définies dans les listes jointes, il faut construire la propriété ProjectedFields. Dans l'exemple ci-dessus, la jointure simple se fait sur les listes Customers et Cities et l'on souhaite récupérer les valeurs "distantes" qui sont le nom de la ville et le pays. Ces deux colonnes font partie de la liste Cities et sont symbolisées par les alias CityTitle et CityCountry.

Il est à noter que les jointures peuvent être de type INNER ou LEFT.

III-D-1-b - Jointure multiple en CAML

SPList CustomerList = Web.Lists["Orders"];
SPQuery CustomerCityQuery = new SPQuery();
CustomerCityQuery.Joins =
"<Join Type='INNER' ListAlias='Customers'>" +
"<Eq>" +
"<FieldRef Name='Customer' RefType='Id' />" +
"<FieldRef List='Customers' Name='ID' />" +
"</Eq>" +
"</Join>" +
"<Join Type='INNER' ListAlias='Cities'>" +
"<Eq>" +
"<FieldRef List='Customers' Name='City' RefType='Id' />" +
"<FieldRef List='Cities' Name='ID' /> " +
"</Eq>" +
"</Join>";

StringBuilder ProjectedFields = new StringBuilder();
ProjectedFields.Append("<Field Name='CityTitle' Type='Lookup' List='Cities' ShowField='Title' />");
ProjectedFields.Append("<Field Name='CityCountry' Type='Lookup' List='Cities' ShowField='Country' />");
ProjectedFields.Append("<Field Name='CustomerTitle' Type='Lookup' List='Customers' ShowField='Title' />");
ProjectedFields.Append("<Field Name='CustomerAge' Type='Lookup' List='Customers' ShowField='Age' />");
CustomerCityQuery.ProjectedFields = ProjectedFields.ToString();

SPListItemCollection Results = CustomerList.GetItems(CustomerCityQuery);
foreach (SPListItem Result in Results)
{
SPFieldLookupValue CityTitle =
new SPFieldLookupValue((Result["CityTitle"] != null) ? Result["CityTitle"].ToString() : "");
SPFieldLookupValue CityCountry =
new SPFieldLookupValue((Result["CityCountry"] != null) ? Result["CityCountry"].ToString() : "");
SPFieldLookupValue CustomerTitle =
new SPFieldLookupValue((Result["CustomerTitle"] != null) ? Result["CustomerTitle"].ToString() : "");
SPFieldLookupValue CustomerAge =
new SPFieldLookupValue((Result["CustomerAge"] != null) ? Result["CustomerAge"].ToString() : "");

Console.WriteLine(string.Format("Customer {0} living in {1} - {2} has ordered #{3}",
CustomerTitle.LookupValue,
CityTitle.LookupValue,
CityCountry.LookupValue,
Result.Title));
}

Selon le même principe que les jointures simples, vous spécifiez la valeur de .Joins et de ProjectedFields.

Note: bien que les jointures mutliples soient supportées en CAML et pourraient donc être "traduites" directement par SPLINQ sans passer par des étapes complémentaires, SPLINQ considère néanmoins ces jointures comme inefficaces. Ceci reste, à mon sens, assez mystérieux mais force est de constater qu'il n'est pas possible de les faire exécuter directement par SPLINQ alors qu'il aurait pu ne gérer que du CAML.

IV - LINQ et la mise à jour de données

IV-A - Insertion d'éléments

L'API LINQ permet non seulement de récupérer les données en lecture mais également en écriture. Par exemple, pour ajouter un nouveau client, vous pouvez exécuter ce code:

static void AddCustomer(string CustomerName)
{
try
{
EntityList<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers");
CustomersItem NewCustomer = new CustomersItem();
NewCustomer.Title = CustomerName;
Customers.InsertOnSubmit(NewCustomer);
Ctx.SubmitChanges();
Console.WriteLine("Customer added");
}
catch (SPDuplicateValuesFoundException)
{
Console.WriteLine("The customer was not added because it would have created a duplicate entry");
}
catch (ChangeConflictException ConflictException)
{
Console.WriteLine("The customer was not added because a conflict occured:" + ConflictException.Message);
}
catch (Exception Ex)
{
Console.WriteLine("The customer was not added because the following error occured:" + Ex.Message);
}
}

Comme pour l'acquisition de données, il faut instancier une entité et ensuite, simplement modifier les propriétés souhaitées. La méthode SubmitChanges du data context permettra d'appliquer les modifications.

Il est possible de traiter les différentes exceptions qui peuvent être générées par la mise à jour. Parmi celles-ci, il y a notamment:

1. SPDuplicateValuesFoundException : comme son nom l'indique, cette exception est générée si vous tentez d'insérer un doublon dans une colonne dont l'unicité est obligatoire

2. ChangeConflictException : un conflit est détecté lors de la mise à jour, reportez-vous à la section abordant la gestion de conflits

3. Exception : une exception générique...

Vous pouvez bien sûr combiner plusieurs mises à jour sur différentes entités en même temps et n'appeler qu'une seule fois la méthode SubmitChanges(). Par exemple:

try
{
EntityList<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers");
for(int i=0;i<10;i++)
{
CustomersItem NewCustomer = new CustomersItem();
NewCustomer.Title = String.Format("Customer {0}",i.ToString());
Customers.InsertOnSubmit(NewCustomer);
}
Ctx.SubmitChanges();
Console.WriteLine("Customer added");
}
catch (SPDuplicateValuesFoundException)
{
Console.WriteLine("The customer was not added because it would have created a duplicate entry");
}
catch (ChangeConflictException ConflictException)
{
Console.WriteLine("The customer was not added because a conflict occured:" + ConflictException.Message);
}
catch (Exception Ex)
{
Console.WriteLine("The customer was not added because the following error occured:" + Ex.Message);
}

Ajoutera dix clients d'un coup via un seul appel à SubmitChanges. Si vous travaillez avec plusieurs entités, le principe reste le même.

IV-B - Mise à jour d'un élément

static void UpdateCustomer(int CustomerId, string CityName)
{
EntityList<CitiesItem> Cities = Ctx.GetList<CitiesItem>("Cities");
var CitiesItm = from City in Cities
where City.Title == CityName
select City;

CitiesItem CityItem = null;
foreach (var Cit in CitiesItm)
CityItem = Cit;

if (CityItem == null)
{
Console.WriteLine("City not found");
Environment.Exit(0);
}

EntityList<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers");
var CustomerItems = from Customer in Customers
where Customer.Id == CustomerId
select Customer;

List<CustomersItem> Results = CustomerItems.ToList();
if (Results.Count > 0)
{

try
{
Results.ForEach(CustomerItem => CustomerItem.City = CityItem);
Ctx.SubmitChanges();
Console.WriteLine("Customer {0} updated with City {1}!",
CustomerId.ToString(), CityName);
}
catch (ChangeConflictException ConflictException)
{
Console.WriteLine("The customer was not updated because a conflict occured:" + ConflictException.Message);
}
catch (Exception Ex)
{
Console.WriteLine("The customer was not updated because the following error occured:" + Ex.Message);
}
}
else
{
Console.WriteLine("Customer {0} not found!", CustomerId.ToString());
}
}

De manière similaire, vous voyez à quel point il est aisé de modifier un élément et d'appeler ensuite SubmitChanges

IV-C - Suppression d'un élément

static void DeleteCustomer(int CustomerId)
{
EntityList<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers");
var QueryResults = from Customer in Customers
where Customer.Id == CustomerId
select Customer;
List<CustomersItem> ReturnedCustomers = QueryResults.ToList();
if (ReturnedCustomers.Count > 0)
{
try
{
Customers.DeleteOnSubmit(ReturnedCustomers[0]);
Ctx.SubmitChanges();
}
catch (ChangeConflictException ConflictException)
{
Console.WriteLine("The customers were not updated because a conflict occured:" + ConflictException.Message);
}
catch (Exception Ex)
{
Console.WriteLine("The customers were not updated because the following error occured:" + Ex.Message);
}
}
else
{
Console.WriteLine("Customer {0} not found", CustomerId.ToString());
}
}

Cette fois, vous devez appeler DeleteOnSubmit() et SubmitChanges()

IV-D - Mise à jour massive

Une mise à jour massive consiste à modifier/insérer plusieurs éléments en un seul traitement. On pourrait parler de mise à jour massive lorsqu'un processus modifie une centaine d'éléments par exemple.

En CAML classique, il était préconisé d'utiliser un batch et d'appeler SPWeb.ProcessBatchData pour éviter tout problème de performance. Avec LINQ, il est à ce stade trop tôt (il faut attendre la RTM) pour savoir si ces recommandations perdurent ou si vous pouvez vous fier entièrement à LINQ.

En attendant la réponse à la question ci-dessus, voici un exemple de code effectuant une mise à jour sur tous les clients de la liste Customers pour leur affecter la ville CityName passée en paramètre

static void UpdateAllCustomers(string CityName)
{
EntityList<CitiesItem> Cities = Ctx.GetList<CitiesItem>("Cities");
var CitiesItm = from City in Cities
where City.Title == CityName
select City;

CitiesItem CityItem = null;
foreach (var Cit in CitiesItm)
CityItem = Cit;

if (CityItem == null)
{
Console.WriteLine("City not found");
Environment.Exit(0);
}

EntityList<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers");
var CustomerItems = from Customer in Customers
select Customer;

List<CustomersItem> Results = CustomerItems.ToList();
if (Results.Count > 0)
{

try
{
Results.ForEach(CustomerItem => CustomerItem.City = CityItem);
Ctx.SubmitChanges();
Console.WriteLine("All the customer were updated with City {0}!",
CityName);
}
catch (ChangeConflictException ConflictException)
{
Console.WriteLine("The customers were not updated because a conflict occured:" + ConflictException.Message);
}
catch (Exception Ex)
{
Console.WriteLine("The customers were not updated because the following error occured:" + Ex.Message);
}
}
else
{
Console.WriteLine("No customer currently exist");
}
}

IV-E - Gestion des conflits de mise à jour

Voici un schéma illustrant le cycle menant à un conflit:

Pour pouvoir simuler cette situation, nous avons donc besoin de deux instances parallèles. L'application disponible en téléchargement utilise donc le multi-threading. Voici en détail comment l'application génère la situation de conflit:

Vous saisissez l'age et le client pour qui il faut appliquer la mise à jour. Ensuite, vous double-cliquez sur l'option de votre choix dans la liste des conflicts.

Quel que soit ce choix, le principe sera le même, seule la méthode appelée changera. Par exemple, pour lister les conflits, les threads sont démarrés comme suit:

Thread t1 = new Thread (new ParameterizedThreadStart(ListConflicts));
t1.Start(string.Format("{0};{1};{2}", 2000, NewAge.Text, CustomerList.SelectedValue));
Thread t2 = new Thread (new ParameterizedThreadStart(ListConflicts));
t2.Start(string.Format("{0};{1};{2}", 0, ComputedAge, CustomerList.SelectedValue));

Les paramètres correspondent au temps de pause pour chaque thread, à la valeur de la colonne age et à la valeur du client sélectionné. Comme vous pouvez le constater, le premier thread démarre en premier mais fera une pause de 2 secondes alors que le deuxième ne fera pas de pause. La valeur de ComputedAge est égale à l'age entré + 1.

De cette manière, le premier thread obtiendra l'élément en premier mais le deuxième le modifiera avant le premier ce qui génèrera un conflit. Afin que vous ayez toutes les pièces du puzzle, voici l'implémentation de ListConflicts:

void ListConflicts(object data)
{
string [] Args = data.ToString().Split(';');
SPLINQ ThreadedLinqExample = new SPLINQ(UrlValue.Text);
ThreadedLinqExample.TestChangeConflictExceptionListConflicts(
Convert.ToInt16(Args[0]),
Convert.ToDouble(Args[1]),
Convert.ToInt16(Args[2]));

}

La méthode récupère les paramètres et appelle la méthode TestChangeConflictExceptionListConflicts dont l'implémentation est expliquée dans la section suivante Ce principe est appliquée à toutes les autres méthodes de gestion de conflits.

IV-E-1 - Détection des conflits

Chaque thread démarré branchera donc la méthode suivante qui a pour but de modifier le client passé en paramètre et de lui attribuer un nouvel age:

EntityList<CustomersItem> Customers = Ctx.GetList<CustomersItem>("Customers");
var CustomerItems = from Customer in Customers
where Customer.Id == CustomerId
select Customer;

foreach (var CustomerItem in CustomerItems)
{
CustomerItem.Age = Age;
}
System.Threading.Thread.Sleep(SleepTime);
try
{
Ctx.SubmitChanges(ConflictMode.FailOnFirstConflict);
}
catch (ChangeConflictException)
{

foreach (ObjectChangeConflict UpdatedListItem in Ctx.ChangeConflicts)
{
foreach (MemberChangeConflict UpdatedField in UpdatedListItem.MemberConflicts)
{
MessageBox.Show(string.Format(
"Conflict detected on <{0}>, Original value <{1}>, Database Value <{2}> Trying to set <{3}>",
UpdatedField.Member.Name,
UpdatedField.OriginalValue,
UpdatedField.DatabaseValue,
UpdatedField.CurrentValue));

}
}

}

Lorsque le thread 1 essayera d'appliquer la mise à jour, il branchera le bloc Catch et chaque conflit sera récupéré via la collection de conflits de l'objet datacontext, en l'occurrence Ctx. La méthode ne fait que lister les conflits (dans ce cas-ci il n'y en aura qu'un).

Le message sera donc:

On constate que la valeur de la base de données est déjà 23, à savoir l'age saisi + 1. Le thread 2 a donc bel et bien effectué sa mise à jour. On essaye d'attribuer la valeur 22 qui n'est pas confirmée car nous ne prenons aucune action particulière sur ce conflit. Le comportement par défaut consiste à annuler la mise à jour en cas de conflit. Le résultat final de cette opération sera que le client aura 23 ans, à savoir la valeur attribuée par le 2ème thread.

IV-E-3 - Résolution des conflits - Confirmation de mise à jour

Le principe étant le même que pour l'exemple précédent, voici simplement le code permettant de confirmer ses changements en cas de conflits

try
{
Ctx.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException)
{
MessageBox.Show ("Detected change conflict exception, forcing changes");
Ctx.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);
Ctx.SubmitChanges();
}

En appelant la méthode ResolveAll avec le paramètre RefreshMode.KeepChanges, les mises à jour seront confirmées.

IV-E-4 - Résolution des conflits - Confirmation/Annulation des mises à jour

Si vous effectuez plusieurs mises à jour générant plusieurs conflits, il est possible de choisir selon le conflit si vous souhaitez annuler ou confirmer vos modifications en fonction de critères de votre choix. Dans l'exemple suivant, on voit comment caster un conflit en entité et pouvoir ainsi travailler sur les propriétés de l'objet métier.

Dans ce cas-ci, on décide de confirmer le changement si l'ID du client est 1 sinon on annule les modifications.

try
{
Ctx.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException)
{
foreach (ObjectChangeConflict UpdatedListItem in Ctx.ChangeConflicts)
{
CustomersItem CurrentObj = UpdatedListItem.Object as CustomersItem;
MessageBox.Show(string.Format("Detected conflict for customer <{0}>",
CurrentObj.Id));

foreach (MemberChangeConflict UpdatedField in UpdatedListItem.MemberConflicts)
{
MessageBox.Show(string.Format(
"Conflict detected on <{0}>, Original value <{1}>, Database Value <{2}> Trying to set <{3}>",
UpdatedField.Member.Name,
UpdatedField.OriginalValue,
UpdatedField.DatabaseValue,
UpdatedField.CurrentValue));

if (CurrentObj.Id == 1 &&
UpdatedField.Member.Name == "Age") // in this case it's always age but it's just to show an example
{
MessageBox.Show("Forcing changes for customer <1>");
UpdatedField.Resolve(RefreshMode.KeepCurrentValues);
}
else
{
MessageBox.Show(string.Format("Cancelling changes for customer <{0}>",
CurrentObj.Id));

UpdatedField.Resolve(RefreshMode.OverwriteCurrentValues);
}
}
}
Ctx.SubmitChanges();
}

V - Et le CAML dans tout cela?

V-A - Compatibilité arrière

A priori, tout ce qu'il était possible de faire en CAML dans la version précédente reste possible dans la version 2010. LINQ génère d'ailleurs du CAML pour exécuter ses requêtes.

Outre les requêtes CAML, le language CAML reste toujours d'actualité notamment pour la déclaration des features, custom actions etc....

V-B - Faut-il encore utiliser les requêtes CAML?

Il est à ce stade encore très difficile de répondre à cette question. Dans la version 2007, il était fortement conseillé de travailler avec les objets SPQuery, SPSiteDataQuery et CrossListQueryInfo pour manipuler de gros volumes de données et ce, pour une question de performance.

Pour les mises à jour de masse, il était également conseillé d'utiliser ProcessBatchData et d'empiler les mises à jour (INSERT, UPDATE, DELETE) dans des paquets batch, toujours pour des questions de performance.

Cette nouvelle API LINQ permet de répondre à tous ces besoins, cependant, tant qu'une version RTM n'est pas sortie, il est difficile de savoir si "l'overhead" de performance induit par cette nouvelle couche sera petit, moyen ou grand et si le grain de productivité en terme de développement ne se paiera pas au prix d'une perte de performance notable.

Je dirais que pour tout ce qui est acquisition de données et mises à jour "modestes", il serait certainement stupide de se passer de cette nouvelle API. Pour toute manipulation massive ou espaces à très grosse fréquentation, seul l'avenir nous permettra de savoir si le CAML doit encore être privilégié dans certains cas ou si cette nouvelle API répondra de manière efficace à tous les besoins.

Quoi qu'il en soit, nul doute que cette API combinée aux possibilités relationnelles représente une réelle avancée et permettra d'appréhender plus facilement des besoins applicatifs lorsque les données sont stockées dans SharePoint.

VI - Téléchargement

Vous pouvez télécharger le programme console et la solution utilisateur ici.

Veillez ensuite à suivre les instructions suivantes:

- Extrayez l'archive

- Chargez la solution SPLINQ.wsp dans la galerie de solutions utilisateur d'un site racine et activez-là

- Créez un site basé sur la solution

- Exécutez le programme en spécifiant l'URL du site que vous venez de créer.