LINQ to SQL : LINQ (Language-Integrated Query) .NET pour les données relationnelles
Dinesh Kulkarni, Luca Bolognese, Matt Warren, Anders Hejlsberg, Kit George
Mars 2007
S’applique à :
Nom de code Visual Studio « Orcas »
.NET Framework 3.5
Résumé : LINQ to SQL fournit une infrastructure d’exécution pour gérer les données relationnelles en tant qu’objets sans perdre la possibilité d’interroger. Votre application est libre de manipuler les objets pendant que LINQ to SQL reste en arrière-plan pour suivre automatiquement vos modifications. (119 pages imprimées)
Contenu
Introduction
Visite guidée rapide
Création de classes d’entités
The DataContext
Définition de relations
Interrogation de relations
Modification et enregistrement d’entités
Requêtes In-Depth
Exécution des requêtes
Identité d'un objet
Relations
Jointures
Projections
Requêtes compilées
Traduction SQL
Cycle de vie de l’entité
Suivi des modifications
Envoi de modifications
Modifications simultanées
Transactions
Procédures stockées
Classes d’entités In-Depth
Utilisation d'attributs
Cohérence des graphiques
Notifications de modification
Héritage
Rubriques avancées
Création de bases de données
Interopérabilité avec ADO.NET
Résolution des conflits de modification
Appel de procédures stockées
Outil générateur de classe d’entité
Informations de référence dbML de l’outil générateur
Entités multiniveau
Mappage externe
Prise en charge et notes de la fonction NET Framework
Prise en charge du débogage
Introduction
La plupart des programmes écrits aujourd’hui manipulent les données d’une manière ou d’une autre et ces données sont souvent stockées dans une base de données relationnelle. Pourtant, il existe un énorme fossé entre les langages de programmation modernes et les bases de données dans la façon dont ils représentent et manipulent l’information. Cette incompatibilité d’impédance est visible de plusieurs façons. Le plus notable est que les langages de programmation accèdent aux informations dans les bases de données via des API qui nécessitent que les requêtes soient spécifiées sous forme de chaînes de texte. Ces requêtes sont des parties significatives de la logique du programme. Pourtant, ils sont opaques par le langage et ne peuvent pas tirer parti des fonctionnalités de vérification au moment de la compilation et de conception telles qu’IntelliSense.
Bien sûr, les différences vont beaucoup plus loin que cela. La façon dont les informations sont représentées (le modèle de données) est très différente entre les deux. Les langages de programmation modernes définissent des informations sous forme d’objets. Les bases de données relationnelles utilisent des lignes. Les objets ont une identité unique, car chaque instance est physiquement différent d’un autre. Les lignes sont identifiées par des valeurs de clé primaire. Les objets ont des références qui identifient et relient les instances. Les lignes sont laissées intentionnellement distinctes, ce qui nécessite que les lignes associées soient liées librement à l’aide de clés étrangères. Les objets sont autonomes, existants tant qu’ils sont toujours référencés par un autre objet. Les lignes existent en tant qu’éléments de tables et disparaissent dès qu’elles sont supprimées.
Il n’est pas étonnant que les applications censées combler cet écart soient difficiles à créer et à gérer. Cela simplifierait certainement l’équation de se débarrasser d’un côté ou de l’autre. Pourtant, les bases de données relationnelles fournissent une infrastructure critique pour le stockage à long terme et le traitement des requêtes, et les langages de programmation modernes sont indispensables pour le développement agile et le calcul riche.
Jusqu’à présent, le développeur d’applications a dû résoudre cette incompatibilité dans chaque application séparément. Les meilleures solutions jusqu’à présent ont été des couches d’abstraction de base de données élaborées qui traversent les informations entre les modèles objet spécifiques au domaine des applications et la représentation tabulaire de la base de données, remodelant et reformatant les données de chaque façon. Pourtant, en masant la véritable source de données, ces solutions finissent par jeter la fonctionnalité la plus attrayante des bases de données relationnelles ; la possibilité pour les données d’être interrogées.
LINQ to SQL, un composant de Visual Studio Code Name « Orcas », fournit une infrastructure d’exécution permettant de gérer les données relationnelles en tant qu’objets sans perdre la possibilité d’interroger. Pour ce faire, il traduit des requêtes intégrées au langage en SQL pour une exécution par la base de données, puis traduit les résultats tabulaires en objets que vous définissez. Votre application est alors libre de manipuler les objets pendant que LINQ to SQL reste en arrière-plan pour suivre automatiquement vos modifications.
- LINQ to SQL est conçu pour être non intrusif pour votre application.
- Il est possible de migrer les solutions ADO.NET actuelles vers LINQ to SQL de manière fragmentaire (partage des mêmes connexions et transactions), car LINQ to SQL est simplement un autre composant de la famille ADO.NET. LINQ to SQL dispose également d’une prise en charge étendue des procédures stockées, ce qui permet la réutilisation des ressources d’entreprise existantes.
- LINQ to SQL applications sont faciles à démarrer.
- Les objets liés à des données relationnelles peuvent être définis comme des objets normaux, décorés uniquement avec des attributs pour identifier la façon dont les propriétés correspondent aux colonnes. Bien sûr, il n’est même pas nécessaire de le faire à la main. Un outil au moment du design est fourni pour automatiser la traduction de schémas de base de données relationnelle préexistants en définitions d’objets.
Ensemble, le LINQ to SQL infrastructure d’exécution et les outils de conception réduisent considérablement la charge de travail du développeur d’applications de base de données. Les chapitres suivants fournissent une vue d’ensemble de la façon dont LINQ to SQL peuvent être utilisés pour effectuer des tâches courantes liées à la base de données. Il est supposé que le lecteur est familiarisé avec Language-Integrated Query et les opérateurs de requête standard.
LINQ to SQL est indépendant du langage. N’importe quel langage conçu pour fournir Language-Integrated Query peut l’utiliser pour activer l’accès aux informations stockées dans des bases de données relationnelles. Les exemples de ce document sont affichés à la fois en C# et en Visual Basic ; LINQ to SQL pouvez également être utilisé avec la version linQ du compilateur Visual Basic.
Visite guidée rapide
La première étape de la création d’une application LINQ to SQL consiste à déclarer les classes d’objets que vous allez utiliser pour représenter vos données d’application. Prenons un exemple.
Création de classes d’entités
Nous allons commencer par une classe simple Customer et l’associer à la table customers dans l’exemple de base de données Northwind. Pour ce faire, il suffit d’appliquer un attribut personnalisé en haut de la déclaration de classe. LINQ to SQL définit l’attribut Table à cet effet.
C#
[Table(Name="Customers")]
public class Customer
{
public string CustomerID;
public string City;
}
Visual Basic
<Table(Name:="Customers")> _
Public Class Customer
Public CustomerID As String
Public City As String
End Class
L’attribut Table a une propriété Name que vous pouvez utiliser pour spécifier le nom exact de la table de base de données. Si aucune propriété Name n’est fournie, LINQ to SQL supposez que la table de base de données porte le même nom que la classe . Seules les instances de classes déclarées en tant que tables seront stockées dans la base de données. Les instances de ces types de classes sont appelées entités. Les classes elles-mêmes sont appelées classes d’entité.
En plus d’associer des classes à des tables, vous devez indiquer chaque champ ou propriété que vous envisagez d’associer à une colonne de base de données. Pour cela, LINQ to SQL définit l’attribut Column.
C#
[Table(Name="Customers")]
public class Customer
{
[Column(IsPrimaryKey=true)]
public string CustomerID;
[Column]
public string City;
}
Visual Basic
<Table(Name:="Customers")> _
Public Class Customer
<Column(IsPrimaryKey:=true)> _
Public CustomerID As String
<Column> _
Public City As String
End Class
L’attribut Column a diverses propriétés que vous pouvez utiliser pour personnaliser le mappage exact entre vos champs et les colonnes de base de données. L’une des propriétés à noter est la propriété Id . Il indique LINQ to SQL que la colonne de base de données fait partie de la clé primaire dans la table.
Comme avec l’attribut Table , vous devez uniquement fournir des informations dans l’attribut Column s’il diffère de ce qui peut être déduit de votre déclaration de champ ou de propriété. Dans cet exemple, vous devez indiquer à LINQ to SQL que le champ CustomerID fait partie de la clé primaire dans la table, mais que vous n’avez pas besoin de spécifier le nom ou le type exact.
Seuls les champs et propriétés déclarés en tant que colonnes sont conservés ou récupérés à partir de la base de données. D’autres sont considérées comme des parties temporaires de votre logique d’application.
The DataContext
Le DataContext est le canal main par lequel vous récupérez des objets de la base de données et renvoyez les modifications. Vous l’utilisez de la même façon qu’une connexion ADO.NET. En fait, le DataContext est initialisé avec une connexion ou une chaîne de connexion que vous fournissez. L’objectif du DataContext est de traduire vos demandes d’objets en requêtes SQL effectuées sur la base de données, puis d’assembler des objets à partir des résultats. DataContext active la requête intégrée au langage en implémentant le même modèle d’opérateur que les opérateurs de requête standard tels que Where et Select.
Par exemple, vous pouvez utiliser le DataContext pour récupérer les objets clients dont la ville est Londres comme suit :
C#
// DataContext takes a connection string
DataContext db = new DataContext("c:\\northwind\\northwnd.mdf");
// Get a typed table to run queries
Table<Customer> Customers = db.GetTable<Customer>();
// Query for customers from London
var q =
from c in Customers
where c.City == "London"
select c;
foreach (var cust in q)
Console.WriteLine("id = {0}, City = {1}", cust.CustomerID, cust.City);
Visual Basic
' DataContext takes a connection string
Dim db As DataContext = New DataContext("c:\northwind\northwnd.mdf")
' Get a typed table to run queries
Dim Customers As Customers(Of Customer) = db.GetTable(Of Customer)()
' Query for customers from London
Dim londonCustomers = From customer in Customers _
Where customer.City = "London" _
Select customer
For Each cust in londonCustomers
Console.WriteLine("id = " & cust.CustomerID & ", City = " & cust.City)
Next
Chaque table de base de données est représentée sous la forme d’une collection Table , accessible via la méthode GetTable() à l’aide de sa classe d’entité pour l’identifier. Il est recommandé de déclarer un DataContext fortement typé au lieu de vous appuyer sur la classe DataContext de base et la méthode GetTable(). Un DataContext fortement typé déclare toutes les collections Table en tant que membres du contexte.
C#
public partial class Northwind : DataContext
{
public Table<Customer> Customers;
public Table<Order> Orders;
public Northwind(string connection): base(connection) {}
}
Visual Basic
Partial Public Class Northwind
Inherits DataContext
Public Customers As Table(Of Customers)
Public Orders As Table(Of Orders)
Public Sub New(ByVal connection As String)
MyBase.New(connection)
End Sub
End Class
La requête pour les clients de Londres peut alors être exprimée plus simplement comme suit :
C#
Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach (var cust in q)
Console.WriteLine("id = {0}, City = {1}",cust.CustomerID, cust.City);
Visual Basic
Dim db = New Northwind("c:\northwind\northwnd.mdf")
Dim londonCustomers = From cust In db.Customers _
Where cust.City = "London" _
Select cust
For Each cust in londonCustomers
Console.WriteLine("id = {0}, City = {1}", cust.CustomerID, cust.City)
Next
Nous continuerons à utiliser la classe Northwind fortement typée pour le reste du document de présentation.
Définition de relations
Les relations dans les bases de données relationnelles sont généralement modélisées en tant que valeurs de clé étrangère faisant référence aux clés primaires dans d’autres tables. Pour naviguer entre elles, vous devez rassembler explicitement les deux tables à l’aide d’une opération de jointure relationnelle. Les objets, en revanche, se réfèrent les uns aux autres à l’aide de références de propriété ou de collections de références parcourues à l’aide de la notation « point ». Évidemment, le dotting est plus simple que la jointure, car vous n’avez pas besoin de rappeler la condition de jointure explicite chaque fois que vous naviguez.
Pour les relations de données telles que celles-ci qui seront toujours les mêmes, il devient très pratique de les encoder en tant que références de propriété dans votre classe d’entité. LINQ to SQL définit un attribut Association que vous pouvez appliquer à un membre utilisé pour représenter une relation. Une relation d’association est une relation comme une relation de clé étrangère à clé primaire qui est établie en faisant correspondre des valeurs de colonne entre des tables.
C#
[Table(Name="Customers")]
public class Customer
{
[Column(Id=true)]
public string CustomerID;
...
private EntitySet<Order> _Orders;
[Association(Storage="_Orders", OtherKey="CustomerID")]
public EntitySet<Order> Orders {
get { return this._Orders; }
set { this._Orders.Assign(value); }
}
}
Visual Basic
<Table(Name:="Customers")> _
Public Class Customer
<Column(Id:=true)> _
Public CustomerID As String
...
Private _Orders As EntitySet(Of Order)
<Association(Storage:="_Orders", OtherKey:="CustomerID")> _
Public Property Orders() As EntitySet(Of Order)
Get
Return Me._Orders
End Get
Set(ByVal value As EntitySet(Of Order))
End Set
End Property
End Class
La classe Customer a maintenant une propriété qui déclare la relation entre les clients et leurs commandes. La propriété Orders est de type EntitySet , car la relation est un-à-plusieurs. Nous utilisons la propriété OtherKey dans l’attribut Association pour décrire la façon dont cette association est effectuée. Il spécifie les noms des propriétés de la classe associée à comparer à celle-ci. Il y avait également une propriété ThisKey que nous n’avons pas spécifiée. Normalement, nous l’utiliserions pour répertorier les membres de ce côté de la relation. Toutefois, en l’omettant, nous permettons LINQ to SQL de les déduire des membres qui composent la clé primaire.
Notez comment cela est inversé dans la définition de la classe Order .
C#
[Table(Name="Orders")]
public class Order
{
[Column(Id=true)]
public int OrderID;
[Column]
public string CustomerID;
private EntityRef<Customer> _Customer;
[Association(Storage="_Customer", ThisKey="CustomerID")]
public Customer Customer {
get { return this._Customer.Entity; }
set { this._Customer.Entity = value; }
}
}
Visual Basic
<Table(Name:="Orders")> _
Public Class Order
<Column(Id:=true)> _
Public OrderID As String
<Column> _
Public CustomerID As String
Private _Customer As EntityRef(Of Customer)
<Association(Storage:="_Customer", ThisKey:="CustomerID")> _
Public Property Customer() As Customer
Get
Return Me._Customer.Entity
End Get
Set(ByVal value As Customer)
Me._Customers.Entity = value
End Set
End Property
End Class
La classe Order utilise le type EntityRef pour décrire la relation avec le client. L’utilisation de la classe EntityRef est nécessaire pour prendre en charge le chargement différé (abordé plus loin). L’attribut Association de la propriété Customer spécifie la propriété ThisKey , car les membres non inférences se trouvent désormais de ce côté de la relation.
Examinez également la propriété Storage . Il indique LINQ to SQL membre privé utilisé pour conserver la valeur de la propriété . Cela permet LINQ to SQL de contourner vos accesseurs de propriété publique lorsqu’il stocke et récupère leur valeur. Cela est essentiel si vous souhaitez LINQ to SQL éviter toute logique métier personnalisée écrite dans vos accesseurs. Si la propriété de stockage n’est pas spécifiée, les accesseurs publics sont utilisés à la place. Vous pouvez également utiliser la propriété Storage avec les attributs Column .
Une fois que vous avez introduit des relations dans vos classes d’entités, la quantité de code que vous devez écrire augmente à mesure que vous introduisez la prise en charge des notifications et de la cohérence des graphiques. Heureusement, il existe un outil (décrit plus loin) qui peut être utilisé pour générer toutes les définitions nécessaires en tant que classes partielles, ce qui vous permet d’utiliser un mélange de code généré et de logique métier personnalisée.
Pour le reste de ce document, nous supposons que l’outil a été utilisé pour générer un contexte de données Northwind complet et toutes les classes d’entité.
Interrogation de relations
Maintenant que vous avez des relations, vous pouvez les utiliser lorsque vous écrivez des requêtes simplement en faisant référence aux propriétés de relation définies dans votre classe.
C#
var q =
from c in db.Customers
from o in c.Orders
where c.City == "London"
select new { c, o };
Visual Basic
Dim londonCustOrders = From cust In db.Customers, ord In cust.Orders _
Where cust.City = "London" _
Select Customer = cust, Order = ord
La requête ci-dessus utilise la propriété Orders pour former le produit croisé entre les clients et les commandes, produisant une nouvelle séquence de paires Client et Commande .
Il est également possible de faire l’inverse.
C#
var q =
from o in db.Orders
where o.Customer.City == "London"
select new { c = o.Customer, o };
Visual Basic
Dim londonCustOrders = From ord In db.Orders _
Where ord.Customer.City = "London" _
Select Customer = ord.Customer, Order = ord
Dans cet exemple, les commandes sont interrogées et la relation Client est utilisée pour accéder aux informations sur l’objet Customer associé.
Modification et enregistrement d’entités
Peu d’applications sont générées avec uniquement la requête à l’esprit. Les données doivent également être créées et modifiées. LINQ to SQL est conçu pour offrir une flexibilité maximale dans la manipulation et la persistance des modifications apportées à vos objets. Dès que des objets d’entité sont disponibles, soit en les récupérant via une requête, soit en les construisant à nouveau, vous pouvez les manipuler comme des objets normaux dans votre application, en modifiant leurs valeurs ou en les ajoutant et les supprimant des collections comme vous le souhaitez. LINQ to SQL effectue le suivi de toutes vos modifications et est prêt à les transmettre à la base de données dès que vous avez terminé.
L’exemple ci-dessous utilise les classes Customer et Order générées par un outil à partir des métadonnées de l’ensemble de l’exemple de base de données Northwind. Les définitions de classe n’ont pas été affichées par souci de concision.
C#
Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// Query for a specific customer
string id = "ALFKI";
var cust = db.Customers.Single(c => c.CustomerID == id);
// Change the name of the contact
cust.ContactName = "New Contact";
// Create and add a new Order to Orders collection
Order ord = new Order { OrderDate = DateTime.Now };
cust.Orders.Add(ord);
// Ask the DataContext to save all the changes
db.SubmitChanges();
Visual Basic
Dim db As New Northwind("c:\northwind\northwnd.mdf")
' Query for a specific customer
Dim id As String = "ALFKI"
Dim targetCustomer = (From cust In db.Customers _
Where cust.CustomerID = id).First
' Change the name of the contact
targetCustomer.ContactName = "New Contact"
' Create and add a new Order to Orders collection
Dim id = New Order With { .OrderDate = DateTime.Now }
targetCustomer.Orders.Add(ord)
' Ask the DataContext to save all the changes
db.SubmitChanges()
Lorsque SubmitChanges() est appelé, LINQ to SQL génère et exécute automatiquement des commandes SQL afin de transmettre les modifications à la base de données. Il est également possible de remplacer ce comportement par une logique personnalisée. La logique personnalisée peut appeler une procédure stockée de base de données.
Requêtes In-Depth
LINQ to SQL fournit une implémentation des opérateurs de requête standard pour les objets associés aux tables dans une base de données relationnelle. Ce chapitre décrit les aspects spécifiques LINQ to SQL des requêtes.
Exécution des requêtes
Que vous écriviez une requête en tant qu’expression de requête de haut niveau ou que vous en générez une à partir des opérateurs individuels, la requête que vous écrivez n’est pas une instruction impérative exécutée immédiatement. Il s’agit d’une description. Par exemple, dans la déclaration sous la variable locale q fait référence à la description de la requête et non au résultat de son exécution.
C#
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
Visual Basic
Dim londonCustomers = From cust In db.Customers _
where cust.City = "London"
For Each cust In londonCustomers
Console.WriteLine(cust.CompanyName)
Next
Le type réel de q dans cette instance est IQueryable<Customer>. Ce n’est qu’une fois que l’application tente d’énumérer le contenu de la requête qu’elle s’exécute réellement. Dans cet exemple, l’instruction foreach provoque l’exécution.
Un objet IQueryable est similaire à un objet de commande ADO.NET. En avoir un en main n’implique pas qu’une requête a été exécutée. Un objet de commande conserve une chaîne qui décrit une requête. De même, un objet IQueryable conserve une description d’une requête encodée sous la forme d’une structure de données appelée Expression. Un objet de commande a une méthode ExecuteReader() qui provoque l’exécution, renvoyant les résultats en tant que DataReader. Un objet IQueryable a une méthode GetEnumerator() qui provoque l’exécution, renvoyant les résultats en tant que client> IEnumerator<.
Par conséquent, il s’ensuit que si une requête est énumérée deux fois, elle sera exécutée deux fois.
C#
var q =
from c in db.Customers
where c.City == "London"
select c;
// Execute first time
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
// Execute second time
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
Visual Basic
Dim londonCustomers = From cust In db.Customers _
where cust.City = "London"
' Execute first time
For Each cust In londonCustomers
Console.WriteLine(cust.CompanyName)
Next
' Execute second time
For Each cust In londonCustomers
Console.WriteLine(cust.CustomerID)
Next
Ce comportement est appelé exécution différée. Tout comme avec un objet de commande ADO.NET, il est possible de conserver une requête et de l’exécuter à nouveau.
Bien sûr, les rédacteurs d’applications doivent souvent être très explicites sur l’emplacement et le moment où une requête est exécutée. Il serait inattendu qu’une application exécute une requête plusieurs fois, simplement parce qu’elle a besoin d’examiner les résultats plusieurs fois. Par exemple, vous pouvez lier les résultats d’une requête à quelque chose comme un DataGrid. Le contrôle peut énumérer les résultats chaque fois qu’il peint sur l’écran.
Pour éviter d’exécuter plusieurs fois, convertissez les résultats en un nombre quelconque de classes de collection standard. Il est facile de convertir les résultats en liste ou tableau à l’aide des opérateurs de requête standard ToList() ou ToArray().
C#
var q =
from c in db.Customers
where c.City == "London"
select c;
// Execute once using ToList() or ToArray()
var list = q.ToList();
foreach (Customer c in list)
Console.WriteLine(c.CompanyName);
foreach (Customer c in list)
Console.WriteLine(c.CompanyName);
Visual Basic
Dim londonCustomers = From cust In db.Customers _
where cust.City = "London"
' Execute once using ToList() or ToArray()
Dim londonCustList = londonCustomers.ToList()
' Neither of these iterations re-executes the query
For Each cust In londonCustList
Console.WriteLine(cust.CompanyName)
Next
For Each cust In londonCustList
Console.WriteLine(cust.CompanyName)
Next
L’un des avantages de l’exécution différée est que les requêtes peuvent être construites de façon fragmentée avec une exécution qui ne se produit qu’une fois la construction terminée. Vous pouvez commencer par composer une partie d’une requête, l’affecter à une variable locale, puis parfois continuer à lui appliquer d’autres opérateurs.
C#
var q =
from c in db.Customers
where c.City == "London"
select c;
if (orderByLocation) {
q =
from c in q
orderby c.Country, c.City
select c;
}
else if (orderByName) {
q =
from c in q
orderby c.ContactName
select c;
}
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
Visual Basic
Dim londonCustomers = From cust In db.Customers _
where cust.City = "London"
if orderByLocation Then
londonCustomers = From cust in londonCustomers _
Order By cust.Country, cust.City
Else If orderByName Then
londonCustomers = From cust in londonCustomers _
Order By cust.ContactName
End If
For Each cust In londonCustList
Console.WriteLine(cust.CompanyName)
Next
Dans cet exemple, q commence comme une requête pour tous les clients de Londres. Plus tard, il devient une requête ordonnée en fonction de l’état de l’application. En reportant l’exécution, la requête peut être construite en fonction des besoins exacts de l’application sans nécessiter de manipulation de chaîne risquée.
Identité d'un objet
Les objets du runtime ont une identité unique. Si deux variables font référence au même objet, elles font en fait référence au même objet instance. Pour cette raison, les modifications effectuées via un chemin d’accès via une variable sont immédiatement visibles par l’autre. Les lignes d’une table de base de données relationnelle n’ont pas d’identité unique. Toutefois, ils ont une clé primaire et cette clé primaire peut être unique, ce qui signifie qu’aucune ligne ne peut partager la même clé. Pourtant, cela limite uniquement le contenu de la table de base de données. Par conséquent, tant que nous interagissons uniquement avec les données via des commandes à distance, cela équivaut à environ la même chose.
Toutefois, c’est rarement le cas. Le plus souvent, les données sont sorties de la base de données et sont introduites dans un autre niveau où une application les manipule. Il s’agit clairement du modèle que LINQ to SQL est conçu pour prendre en charge. Lorsque les données sont sorties de la base de données sous forme de lignes, on ne s’attend pas à ce que deux lignes représentant les mêmes données correspondent réellement aux mêmes instances de ligne. Si vous interrogez deux fois un client spécifique, vous obtenez deux lignes de données, chacune contenant les mêmes informations.
Pourtant, avec les objets, vous vous attendez à quelque chose de tout à fait différent. Vous vous attendez à ce que si vous demandez à nouveau au DataContext les mêmes informations, il vous redevient le même objet instance. Vous vous attendez à cela, car les objets ont une signification particulière pour votre application et vous vous attendez à ce qu’ils se comportent comme des objets normaux. Vous les avez conçus comme des hiérarchies ou des graphiques et vous vous attendez certainement à les récupérer en tant que tels, sans hordes d’instances répliquées simplement parce que vous avez demandé la même chose deux fois.
Pour cette raison, dataContext gère l’identité d’objet. Chaque fois qu’une nouvelle ligne est récupérée à partir de la base de données, elle est enregistrée dans une table d’identités par sa clé primaire et un nouvel objet est créé. Chaque fois que cette même ligne est récupérée à nouveau, l’objet d’origine instance est remis à l’application. De cette façon, le DataContext traduit le concept d’identité (clés) des bases de données en concept de langage (instances). L’application ne voit jamais l’objet que dans l’état où il a été récupéré pour la première fois. Les nouvelles données, si elles sont différentes, sont jetées.
Vous pourriez être perplexe à ce sujet, car pourquoi une application jeterait-elle des données ? Il s’avère que c’est ainsi que LINQ to SQL gère l’intégrité des objets locaux et est en mesure de prendre en charge les mises à jour optimistes. Étant donné que les seules modifications qui se produisent après la création initiale de l’objet sont celles apportées par l’application, l’intention de l’application est claire. Si des modifications effectuées par une partie externe se sont produites entre-temps, elles seront identifiées au moment de l’appel de SubmitChanges(). Plus d’informations sont expliquées dans la section Modifications simultanées.
Notez que, dans le cas où la base de données contient une table sans clé primaire, LINQ to SQL autorise l’envoi de requêtes sur la table, mais il n’autorise pas les mises à jour. Cela est dû au fait que l’infrastructure ne peut pas identifier la ligne à mettre à jour en raison de l’absence d’une clé unique.
Bien sûr, si l’objet demandé par la requête est facilement identifiable par sa clé primaire en tant qu’objet déjà récupéré, aucune requête n’est exécutée du tout. La table d’identités agit comme un cache stockant tous les objets précédemment récupérés.
Relations
Comme nous l’avons vu dans la visite rapide, les références à d’autres objets ou collections d’autres objets dans vos définitions de classe correspondent directement aux relations de clé étrangère dans la base de données. Vous pouvez utiliser ces relations lorsque vous interrogez en utilisant simplement la notation par points pour accéder aux propriétés de la relation, en naviguant d’un objet à un autre. Ces opérations d’accès se traduisent par des jointures plus compliquées ou des sous-requêtes corrélées dans le sql équivalent, ce qui vous permet de parcourir votre graphe d’objets pendant une requête. Par exemple, la requête suivante navigue des commandes aux clients afin de limiter les résultats aux commandes des clients localisés à Londres.
C#
var q =
from o in db.Orders
where o.Customer.City == "London"
select o;
Visual Basic
Dim londonOrders = From ord In db.Orders _
where ord.Customer.City = "London"
Si les propriétés de relation n’existaient pas, vous devrez les écrire manuellement en tant que jointures, comme vous le feriez dans une requête SQL.
C#
var q =
from c in db.Customers
join o in db.Orders on c.CustomerID equals o.CustomerID
where c.City == "London"
select o;
Visual Basic
Dim londonOrders = From cust In db.Customers _
Join ord In db.Orders _
On cust.CustomerID Equals ord.CustomerID _
Where ord.Customer.City = "London" _
Select ord
La propriété de relation vous permet de définir cette relation particulière une fois que vous avez activé l’utilisation de la syntaxe de point plus pratique. Toutefois, ce n’est pas la raison pour laquelle les propriétés de relation existent. Ils existent parce que nous avons tendance à définir nos modèles d’objets spécifiques à un domaine en tant que hiérarchies ou graphiques. Les objets sur lesquels nous choisissons de programmer ont des références à d’autres objets. Ce n’est qu’une heureuse coïncidence que, dans la mesure où les relations d’objet à objet correspondent à des relations de style de clé étrangère dans les bases de données, l’accès à la propriété conduit à un moyen pratique d’écrire des jointures.
Par conséquent, l’existence de propriétés de relation est plus importante du côté des résultats d’une requête que dans le cadre de la requête elle-même. Une fois que vous avez la main sur un client particulier, sa définition de classe vous indique que les clients ont des commandes. Par conséquent, lorsque vous examinez la propriété Orders d’un client particulier, vous vous attendez à voir la collection remplie avec toutes les commandes du client, car il s’agit en fait du contrat que vous avez déclaré en définissant les classes de cette façon. Vous vous attendez à voir les commandes là même si vous n’avez pas particulièrement demandé des commandes à l’avance. Vous vous attendez à ce que votre modèle objet conserve l’illusion qu’il s’agit d’une extension en mémoire de la base de données, avec des objets associés immédiatement disponibles.
LINQ to SQL implémente une technique appelée chargement différé afin de maintenir cette illusion. Lorsque vous interrogez un objet, vous récupérez uniquement les objets que vous avez demandés. Les objets connexes ne sont pas automatiquement extraits en même temps. Toutefois, le fait que les objets associés ne soient pas déjà chargés n’est pas observable, car dès que vous tentez d’y accéder, une demande est envoyée pour les récupérer.
C#
var q =
from o in db.Orders
where o.ShipVia == 3
select o;
foreach (Order o in q) {
if (o.Freight > 200)
SendCustomerNotification(o.Customer);
ProcessOrder(o);
}
Visual Basic
Dim shippedOrders = From ord In db.Orders _
where ord.ShipVia = 3
For Each ord In shippedOrders
If ord.Freight > 200 Then
SendCustomerNotification(ord.Customer)
ProcessOrder(ord)
End If
Next
Par exemple, vous pouvez rechercher un ensemble particulier de commandes et n’envoyer qu’occasionnellement une notification par e-mail à des clients particuliers. Vous n’avez pas besoin de récupérer toutes les données client à l’avance à chaque commande. Le chargement différé vous permet de reporter le coût de récupération d’informations supplémentaires jusqu’à ce que vous ayez absolument à le faire.
Bien sûr, le contraire peut aussi être vrai. Vous pouvez avoir une application qui doit examiner les données client et de commande en même temps. Vous savez que vous avez besoin de ces deux groupes de données. Vous savez que votre application va explorer les commandes de chaque client dès que vous les obtenez. Il serait malheureux de déclencher des requêtes individuelles pour les commandes de chaque client. Ce que vous voulez vraiment, c’est que les données de commande soient récupérées avec les clients.
C#
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach (Customer c in q) {
foreach (Order o in c.Orders) {
ProcessCustomerOrder(o);
}
}
Visual Basic
Dim londonCustomers = From cust In db.Customer _
Where cust.City = "London"
For Each cust In londonCustomers
For Each ord In cust.Orders
ProcessCustomerOrder(ord)
End If
Next
Certes, vous pouvez toujours trouver un moyen de joindre des clients et des commandes dans une requête en formant le produit croisé et en récupérant tous les bits relatifs de données sous forme d’une grande projection. Mais les résultats ne seraient pas des entités. Les entités sont des objets avec une identité que vous pouvez modifier, tandis que les résultats sont des projections qui ne peuvent pas être modifiées et conservées. Pire encore, vous récupérez une énorme quantité de données redondantes à mesure que chaque client répète pour chaque commande dans la sortie de jointure aplatissement.
Ce dont vous avez vraiment besoin est un moyen de récupérer un ensemble d’objets connexes en même temps , une partie délimitée d’un graphique afin que vous ne récupériez jamais plus ou moins que ce qui était nécessaire pour l’utilisation prévue.
LINQ to SQL vous permet de demander le chargement immédiat d’une région de votre modèle objet pour cette raison. Pour ce faire, il autorise la spécification d’un DataShape pour un DataContext. La classe DataShape est utilisée pour indiquer à l’infrastructure quels objets récupérer lorsqu’un type particulier est récupéré. Pour ce faire, utilisez la méthode LoadWith comme suit :
C#
DataShape ds = new DataShape();
ds.LoadWith<Customer>(c => c.Orders);
db.Shape = ds;
var q =
from c in db.Customers
where c.City == "London"
select c;
Visual Basic
Dim ds As DataShape = New DataShape()
ds.LoadWith(Of Customer)(Function(c As Customer) c.Orders)
db.Shape = ds
Dim londonCustomers = From cust In db.Customers _
Where cust.City = "London" _
Select cust
Dans la requête précédente, toutes les commandes de tous les clients résidant à Londres sont récupérées lors de l’exécution de la requête, de sorte que l’accès successif à la propriété Orders sur un objet Customer ne déclenche pas de requête de base de données.
La classe DataShape peut également être utilisée pour spécifier des sous-requêtes qui sont appliquées à une navigation de relation. Par exemple, si vous souhaitez récupérer uniquement les commandes qui ont été expédiées aujourd’hui, vous pouvez utiliser la méthode AssociateWith sur le DataShape comme suit :
C#
DataShape ds = new DataShape();
ds.AssociateWith<Customer>(
c => c.Orders.Where(p => p.ShippedDate != DateTime.Today));
db.Shape = ds;
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach(Customer c in q) {
foreach(Order o in c.Orders) {}
}
Visual Basic
Dim ds As DataShape = New DataShape()
ds.AssociateWith(Of Customer)( _
Function(cust As Customer) From cust In db.Customers _
Where order.ShippedDate <> Today _
Select cust)
db.Shape = ds
Dim londonCustomers = From cust In db.Customers _
Where cust.City = "London" _
Select cust
For Each cust in londonCustomers
For Each ord In cust.Orders …
Next
Next
Dans le code précédent, l’instruction foreach interne itère juste au-dessus des commandes qui ont été expédiées aujourd’hui, car de telles commandes ont été récupérées à partir de la base de données.
Il est important de noter deux faits sur la classe DataShape :
Après avoir affecté un DataShape à un DataContext, le DataShape ne peut pas être modifié. Tout appel de méthode LoadWith ou AssociateWith sur un tel DataShape retourne une erreur au moment de l’exécution.
Il est impossible de créer des cycles à l’aide de LoadWith ou AssociateWith. Par exemple, les éléments suivants génèrent une erreur au moment de l’exécution :
C#
DataShape ds = new DataShape(); ds.AssociateWith<Customer>( c=>c.Orders.Where(o=> o.Customer.Orders.Count() < 35);
Visual Basic
Dim ds As DataShape = New DataShape() ds.AssociateWith(Of Customer)( _ Function(cust As Customer) From ord In cust.Orders _ Where ord.Customer.Orders.Count() < 35)
Jointures
La plupart des requêtes sur les modèles objet s’appuient fortement sur la navigation dans les références d’objets dans le modèle objet. Toutefois, il existe des « relations » intéressantes entre des entités qui ne peuvent pas être capturées dans le modèle objet en tant que références. Par exemple , Customer.Orders est une relation utile basée sur les relations de clé étrangère dans la base de données Northwind. Toutefois, fournisseurs et clients dans la même ville ou le même pays est une relation ad hoc qui n’est pas basée sur une relation de clé étrangère et ne peut pas être capturée dans le modèle objet. Les jointures fournissent un mécanisme supplémentaire pour gérer ces relations. LINQ to SQL prend en charge les nouveaux opérateurs de jointure introduits dans LINQ.
Tenez compte du problème suivant : recherchez des fournisseurs et des clients basés dans la même ville. La requête suivante retourne les noms d’entreprise des fournisseurs et des clients, ainsi que la ville commune sous forme de résultat aplatissement. Il s’agit de l’équivalent de l’équi-jointure interne dans les bases de données relationnelles :
C#
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City
select new {
Supplier = s.CompanyName,
Customer = c.CompanyName,
City = c.City
};
Visual Basic
Dim customerSuppliers = From sup In db.Suppliers _
Join cust In db.Customers _
On sup.City Equals cust.City _
Select Supplier = sup.CompanyName, _
CustomerName = cust.CompanyName, _
City = cust.City
La requête ci-dessus élimine les fournisseurs qui ne se trouvent pas dans la même ville qu’un certain client. Toutefois, il arrive que nous ne voulions pas éliminer l’une des entités dans une relation ad hoc . La requête suivante répertorie tous les fournisseurs avec des groupes de clients pour chacun des fournisseurs. Si un fournisseur particulier n’a pas de client dans la même ville, le résultat est une collection vide de clients correspondant à ce fournisseur. Notez que les résultats ne sont pas plats: chaque fournisseur a une collection associée. En effet, cela fournit une jointure de groupe: il joint deux séquences et regroupe des éléments de la deuxième séquence par les éléments de la première séquence.
C#
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City into scusts
select new { s, scusts };
Visual Basic
Dim customerSuppliers = From sup In db.Suppliers _
Group Join cust In db.Customers _
On sup.City Equals cust.City _
Into supCusts _
Select Supplier = sup, _
Customers = supCusts
La jointure de groupe peut également être étendue à plusieurs collections. La requête suivante étend la requête ci-dessus en listant les employés qui se trouvent dans la même ville que le fournisseur. Ici, le résultat montre un fournisseur avec des collections (éventuellement vides) de clients et d’employés.
C#
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City into scusts
join e in db.Employees on s.City equals e.City into semps
select new { s, scusts, semps };
Visual Basic
Dim customerSuppliers = From sup In db.Suppliers _
Group Join cust In db.Customers _
On sup.City Equals cust.City _
Into supCusts _
Group Join emp In db.Employees _
On sup.City Equals emp.City _
Into supEmps _
Select Supplier = sup, _
Customers = supCusts, Employees = supEmps
Les résultats d’une jointure de groupe peuvent également être aplatits. Les résultats de l’aplatissement de la jonction de groupe entre les fournisseurs et les clients sont plusieurs entrées pour les fournisseurs ayant plusieurs clients dans leur ville, une par client. Les collections vides sont remplacées par des valeurs null. Cela équivaut à une équijoinssion externe gauche dans les bases de données relationnelles.
C#
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City into sc
from x in sc.DefaultIfEmpty()
select new {
Supplier = s.CompanyName,
Customer = x.CompanyName,
City = x.City
};
Visual Basic
Dim customerSuppliers = From sup In db.Suppliers _
Group Join cust In db.Customers _
On sup.City Equals cust.City _
Into supCusts _
Select Supplier = sup, _
CustomerName = supCusts.CompanyName, sup.City
Les signatures des opérateurs de jointure sous-jacents sont définies dans le document opérateurs de requête standard. Seules les jointures equi sont prises en charge et les deux opérandes d’égal doivent avoir le même type.
Projections
Jusqu’à présent, nous n’avons examiné que les requêtes pour récupérer des entités( objets directement associés aux tables de base de données). Nous ne devons pas nous limiter à cela. La beauté d’un langage de requête est que vous pouvez récupérer des informations sous n’importe quelle forme de votre choix. Vous ne pourrez pas tirer parti du suivi automatique des modifications ou de la gestion des identités dans ce cas. Toutefois, vous pouvez obtenir uniquement les données souhaitées.
Par exemple, vous devrez peut-être simplement connaître le nom de l’entreprise de tous les clients de Londres. Si c’est le cas, il n’y a aucune raison particulière de récupérer des objets clients entiers pour simplement sélectionner des noms. Vous pouvez projeter les noms dans le cadre de la requête.
C#
var q =
from c in db.Customers
where c.City == "London"
select c.CompanyName;
Visual Basic
Dim londonCustomerNames = From cust In db.Customer _
Where cust.City = "London" _
Select cust.CompanyName
Dans ce cas, q devient une requête qui récupère une séquence de chaînes.
Si vous souhaitez récupérer plus qu’un seul nom, mais pas assez pour justifier l’extraction de l’objet client entier, vous pouvez spécifier n’importe quel sous-ensemble souhaité en construisant les résultats dans le cadre de votre requête.
C#
var q =
from c in db.Customers
where c.City == "London"
select new { c.CompanyName, c.Phone };
Visual Basic
Dim londonCustomerInfo = From cust In db.Customer _
Where cust.City = "London" _
Select cust.CompanyName, cust.Phone
Cet exemple utilise un initialiseur d’objet anonyme pour créer une structure qui contient à la fois le nom de l’entreprise et le numéro de téléphone. Vous ne savez peut-être pas quoi appeler le type, mais avec la déclaration de variable locale implicitement typée dans la langue, vous n’avez pas nécessairement besoin de le faire.
C#
var q =
from c in db.Customers
where c.City == "London"
select new { c.CompanyName, c.Phone };
foreach(var c in q)
Console.WriteLine("{0}, {1}", c.CompanyName, c.Phone);
Visual Basic
Dim londonCustomerInfo = From cust In db.Customer _
Where cust.City = "London" _
Select cust.CompanyName, cust.Phone
For Each cust In londonCustomerInfo
Console.WriteLine(cust.CompanyName & ", " & cust.Phone)
Next
Si vous consommez les données immédiatement, les types anonymes constituent une bonne alternative à la définition explicite de classes pour contenir les résultats de votre requête.
Vous pouvez également former des produits croisés d’objets entiers, même si vous avez rarement une raison de le faire.
C#
var q =
from c in db.Customers
from o in c.Orders
where c.City == "London"
select new { c, o };
Visual Basic
Dim londonOrders = From cust In db.Customer, _
ord In db.Orders _
Where cust.City = "London" _
Select Customer = cust, Order = ord
Cette requête construit une séquence de paires d’objets client et de commande.
Il est également possible d’effectuer des projections à n’importe quel stade de la requête. Vous pouvez projeter des données dans des objets nouvellement construits, puis faire référence aux membres de ces objets dans les opérations de requête suivantes.
C#
var q =
from c in db.Customers
where c.City == "London"
select new {Name = c.ContactName, c.Phone} into x
orderby x.Name
select x;
Visual Basic
Dim londonItems = From cust In db.Customer _
Where cust.City = "London" _
Select Name = cust.ContactName, cust.Phone _
Order By Name
Méfiez-vous toutefois de l’utilisation de constructeurs paramétrables à ce stade. Il est techniquement valide de le faire, mais il est impossible pour LINQ to SQL de suivre la façon dont l’utilisation du constructeur affecte l’état membre sans comprendre le code réel à l’intérieur du constructeur.
C#
var q =
from c in db.Customers
where c.City == "London"
select new MyType(c.ContactName, c.Phone) into x
orderby x.Name
select x;
Visual Basic
Dim londonItems = From cust In db.Customer _
Where cust.City = "London" _
Select MyType = New MyType(cust.ContactName, cust.Phone) _
Order By MyType.Name
Étant donné que LINQ to SQL tentatives de traduction de la requête en types d’objets SQL relationnels purs définis localement ne sont pas disponibles sur le serveur pour la construction réelle. Toute construction d’objet est en fait reportée jusqu’à ce que les données soient récupérées à partir de la base de données. À la place des constructeurs réels, le sql généré utilise une projection de colonne SQL normale. Étant donné qu’il n’est pas possible pour le traducteur de requête de comprendre ce qui se passe pendant un appel de constructeur, il n’est pas en mesure d’établir une signification pour le champ Name de MyType.
Au lieu de cela, la meilleure pratique consiste à toujours utiliser des initialiseurs d’objet pour encoder les projections.
C#
var q =
from c in db.Customers
where c.City == "London"
select new MyType { Name = c.ContactName, HomePhone = c.Phone } into x
orderby x.Name
select x;
Visual Basic
Dim londonCustomers = From cust In db.Customer _
Where cust.City = "London" _
Select Contact = New With {.Name = cust.ContactName, _
.Phone = cust.Phone} _
Order By Contact.Name
Le seul endroit sûr pour utiliser un constructeur paramétré se trouve dans la projection finale d’une requête.
C#
var e =
new XElement("results",
from c in db.Customers
where c.City == "London"
select new XElement("customer",
new XElement("name", c.ContactName),
new XElement("phone", c.Phone)
)
);
Visual Basic
Dim x = <results>
<%= From cust In db.Customers _
Where cust.City = "London" _
Select <customer>
<name><%= cust.ContactName %></name>
<phone><%= cust.Phone %></phone>
</customer>
%>
</results>
Vous pouvez même utiliser l’imbrication élaborée des constructeurs d’objets si vous le souhaitez, comme cet exemple qui construit du XML directement à partir du résultat d’une requête. Il fonctionne tant qu’il s’agit de la dernière projection de la requête.
Pourtant, même si les appels de constructeur sont compris, les appels aux méthodes locales peuvent ne pas l’être. Si votre projection finale nécessite l’appel de méthodes locales, il est peu probable que LINQ to SQL puisse l’obliger. Les appels de méthode qui n’ont pas de traduction connue en SQL ne peuvent pas être utilisés dans le cadre de la requête. Une exception à cette règle est les appels de méthode qui n’ont aucun argument dépendant des variables de requête. Ceux-ci ne sont pas considérés comme faisant partie de la requête traduite et sont plutôt traités comme des paramètres.
Les projections (transformations) encore élaborées peuvent nécessiter une logique procédurale locale pour l’implémentation. Pour utiliser vos propres méthodes locales dans une projection finale, vous devez projeter deux fois. La première projection extrait toutes les valeurs de données que vous devez référencer et la deuxième projection effectue la transformation. Entre ces deux projections se trouve un appel à l’opérateur AsEnumerable() qui déplace le traitement à ce stade d’une requête LINQ to SQL en une requête exécutée localement.
C#
var q =
from c in db.Customers
where c.City == "London"
select new { c.ContactName, c.Phone };
var q2 =
from c in q.AsEnumerable()
select new MyType {
Name = DoNameProcessing(c.ContactName),
Phone = DoPhoneProcessing(c.Phone)
};
Visual Basic
Dim londonCustomers = From cust In db.Customer _
Where cust.City = "London" _
Select cust.ContactName, cust.Phone
Dim processedCustomers = From cust In londonCustomers.AsEnumerable() _
Select Contact = New With { _
.Name = DoNameProcessing(cust.ContactName), _
.Phone = DoPhoneProcessing(cust.Phone)}
Note L’opérateur AsEnumerable(), contrairement à ToList() et ToArray(), n’entraîne pas l’exécution de la requête. Il est toujours différé. L’opérateur AsEnumerable() modifie simplement le typage statique de la requête, transformant un T> IQueryable< (IQueryable (ofT) en IEnumerable<T>
(IEnumerable (ofT) en Visual Basic, ce qui incite le compilateur à traiter le reste de la requête comme exécuté localement.
Requêtes compilées
Dans de nombreuses applications, il est courant d’exécuter plusieurs fois des requêtes structurellement similaires. Dans ce cas, il est possible d’augmenter les performances en compilant la requête une seule fois et en l’exécutant plusieurs fois dans l’application avec différents paramètres. Ce résultat est obtenu dans LINQ to SQL à l’aide de la classe CompiledQuery. Le code suivant montre comment définir une requête compilée :
C#
static class Queries
{
public static Func<Northwind, string, IQueryable<Customer>>
CustomersByCity = CompiledQuery.Compile((Northwind db, string city) =>
from c in db.Customers where c.City == city select c);
}
Visual Basic
Class Queries
public Shared Function(Of Northwind, String, IQueryable(Of Customer)) _ CustomersByCity = CompiledQuery.Compile( _
Function(db As Northwind, city As String) _
From cust In db.Customers Where cust.City = city)
End Class
La méthode Compile retourne un délégué qui peut être mis en cache et exécuté après plusieurs fois en modifiant simplement les paramètres d’entrée. Le code suivant montre un exemple de ceci :
C#
public IEnumerable<Customer> GetCustomersByCity(string city) {
Northwind db = new Northwind();
return Queries.CustomersByCity(myDb, city);
}
Visual Basic
Public Function GetCustomersByCity(city As String) _
As IEnumerable(Of Customer)
Dim db As Northwind = New Northwind()
Return Queries.CustomersByCity(myDb, city)
End Function
Traduction SQL
LINQ to SQL n’exécute pas réellement de requêtes; c’est le cas de la base de données relationnelle. LINQ to SQL traduit les requêtes que vous avez écrites en requêtes SQL équivalentes et les envoie au serveur pour traitement. Étant donné que l’exécution est différée, LINQ to SQL est en mesure d’examiner l’ensemble de votre requête, même si elle est assemblée à partir de plusieurs parties.
Étant donné que le serveur de base de données relationnelle n’exécute pas réellement il (à l’exception de l’intégration CLR dans SQL Server 2005), les requêtes ne sont pas transmises au serveur en tant qu’il. Elles sont en fait transmises sous forme de requêtes SQL paramétrables sous forme de texte.
Bien sûr, SQL, même T-SQL avec intégration CLR, est incapable d’exécuter les différentes méthodes disponibles localement pour votre programme. Par conséquent, les requêtes que vous écrivez doivent être traduites en opérations et fonctions équivalentes disponibles dans l’environnement SQL.
La plupart des méthodes et opérateurs sur les types intégrés .Net Framework ont des traductions directes dans SQL. Certaines peuvent être générées à partir des fonctions disponibles. Ceux qui ne peuvent pas être traduits ne sont pas autorisés, ce qui génère des exceptions d’exécution si vous essayez de les utiliser. Une section plus loin dans le document détaille les méthodes d’infrastructure implémentées pour la traduction en SQL.
Cycle de vie de l’entité
LINQ to SQL est plus qu’une simple implémentation des opérateurs de requête standard pour les bases de données relationnelles. En plus de traduire des requêtes, il s’agit d’un service qui gère vos objets tout au long de leur durée de vie, ce qui vous aide à maintenir l’intégrité de vos données et à automatiser le processus de traduction de vos modifications dans le magasin.
Dans un scénario classique, les objets sont récupérés via une ou plusieurs requêtes, puis manipulés d’une manière ou d’une autre jusqu’à ce que l’application soit prête à renvoyer les modifications au serveur. Ce processus peut se répéter plusieurs fois jusqu’à ce que l’application ne soit plus utilisée pour ces informations. À ce stade, les objets sont récupérés par le runtime comme les objets normaux. Toutefois, les données restent dans la base de données. Même après avoir été effacés de leur existence au moment de l’exécution, les objets représentant les mêmes données peuvent toujours être récupérés. En ce sens, la durée de vie réelle de l’objet existe au-delà de toute manifestation unique au moment de l’exécution.
Ce chapitre se concentre sur le cycle de vie de l’entité où un cycle fait référence à l’intervalle de temps d’une seule manifestation d’un objet d’entité dans un contexte d’exécution particulier. Le cycle commence lorsque le DataContext prend connaissance d’une nouvelle instance et se termine lorsque l’objet ou dataContext n’est plus nécessaire.
Suivi des modifications
Une fois les entités récupérées à partir de la base de données, vous êtes libre de les manipuler comme vous le souhaitez. Ce sont vos objets; utilisez-les comme vous le souhaitez. En procédant ainsi, LINQ to SQL effectue le suivi des modifications afin qu’elle puisse les conserver dans la base de données lorsque SubmitChanges() est appelé.
LINQ to SQL commence à suivre vos entités dès qu’elles sont récupérées à partir de la base de données, avant que vous ne les mettez la main dessus. En effet, le service de gestion des identités mentionné précédemment a déjà été mis en place. Le suivi des modifications coûte très peu de frais supplémentaires jusqu’à ce que vous commenciez à apporter des modifications.
C#
Customer cust = db.Customers.Single(c => c.CustomerID == "ALFKI");
cust.CompanyName = "Dr. Frogg's Croakers";
Visual Basic
' Query for a specific customer
Dim id As String = "ALFKI"
Dim targetCustomer = (From cust In db.Customers _
Where cust.CustomerID = id).First
targetCustomer.CompanyName = "Dr. Frogg's Croakers"
Dès que companyName est affecté dans l’exemple ci-dessus, LINQ to SQL prend connaissance de la modification et est en mesure de l’enregistrer. Les valeurs d’origine de tous les membres de données sont conservées par le service de suivi des modifications.
Le service de suivi des modifications enregistre également toutes les manipulations des propriétés de relation. Vous utilisez des propriétés de relation pour établir des liens entre vos entités, même si elles peuvent être liées par des valeurs de clé dans la base de données. Il n’est pas nécessaire de modifier directement les membres associés aux colonnes clés. LINQ to SQL les synchronise automatiquement pour vous avant l’envoi des modifications.
C#
Customer cust1 = db.Customers.Single(c => c.CustomerID == custId1);
foreach (Order o in db.Orders.Where(o => o.CustomerID == custId2)) {
o.Customer = cust1;
}
Visual Basic
Dim targetCustomer = (From cust In db.Customers _
Where cust.CustomerID = custId1).First
For Each ord In (From o In db.Orders _
Where o.CustomerID = custId2)
o.Customer = targetCustomer
Next
Vous pouvez déplacer des commandes d’un client à un autre en effectuant simplement une affectation à sa propriété Customer . Étant donné que la relation existe entre le client et la commande, vous pouvez modifier la relation en modifiant l’un ou l’autre côté. Vous auriez pu les supprimer de la collection Commandes de
cust2 et les ajouter à la collection de commandes cust1, comme indiqué ci-dessous.
C#
Customer cust1 = db.Customers.Single(c => c.CustomerID == custId1);
Customer cust2 = db.Customers.Single(c => c.CustomerID == custId2);
// Pick some order
Order o = cust2.Orders[0];
// Remove from one, add to the other
cust2.Orders.Remove(o);
cust1.Orders.Add(o);
// Displays 'true'
Console.WriteLine(o.Customer == cust1);
Visual Basic
Dim targetCustomer1 = (From cust In db.Customers _
Where cust.CustomerID = custId1).First
Dim targetCustomer2 = (From cust In db.Customers _
Where cust.CustomerID = custId1).First
' Pick some order
Dim o As Order = targetCustomer2.Orders(0)
' Remove from one, add to the other
targetCustomer2.Orders.Remove(o)
targetCustomer1.Orders.Add(o)
' Displays 'True'
MsgBox(o.Customer = targetCustomer1)
Bien sûr, si vous affectez à une relation la valeur null, vous vous débarrassez complètement de la relation. L’affectation d’une propriété Customer d’une commande à null supprime en fait la commande de la liste du client.
C#
Customer cust = db.Customers.Single(c => c.CustomerID == custId1);
// Pick some order
Order o = cust.Orders[0];
// Assign null value
o.Customer = null;
// Displays 'false'
Console.WriteLine(cust.Orders.Contains(o));
Visual Basic
Dim targetCustomer = (From cust In db.Customers _
Where cust.CustomerID = custId1).First
' Pick some order
Dim o As Order = targetCustomer.Orders(0)
' Assign null value
o.Customer = Nothing
' Displays 'False'
Msgbox(targetCustomer.Orders.Contains(o))
La mise à jour automatique des deux côtés d’une relation est essentielle pour maintenir la cohérence de votre graphique d’objets. Contrairement aux objets normaux, les relations entre les données sont souvent bidirectionnelles. LINQ to SQL vous permet d’utiliser des propriétés pour représenter des relations. Toutefois, il n’offre pas de service permettant de maintenir automatiquement ces propriétés bidirectionnelles synchronisées. Il s’agit d’un niveau de service qui doit être intégré directement dans vos définitions de classe. Les classes d’entité générées à l’aide de l’outil de génération de code ont cette fonctionnalité. Dans le chapitre suivant, nous allons vous montrer comment procéder pour vos propres classes manuscrites.
Toutefois, il est important de noter que la suppression d’une relation n’implique pas qu’un objet a été supprimé de la base de données. N’oubliez pas que la durée de vie des données sous-jacentes persiste dans la base de données jusqu’à ce que la ligne ait été supprimée de la table. La seule façon de supprimer un objet consiste à le supprimer de sa collection Table .
C#
Customer cust = db.Customers.Single(c => c.CustomerID == custId1);
// Pick some order
Order o = cust.Orders[0];
// Remove it directly from the table (I want it gone!)
db.Orders.Remove(o);
// Displays 'false'.. gone from customer's Orders
Console.WriteLine(cust.Orders.Contains(o));
// Displays 'true'.. order is detached from its customer
Console.WriteLine(o.Customer == null);
Visual Basic
Dim targetCustomer = (From cust In db.Customers _
Where cust.CustomerID = custId1).First
' Pick some order
Dim o As Order = targetCustomer.Orders(0)
' Remove it directly from the table (I want it gone!)
db.Orders.Remove(o)
' Displays 'False'.. gone from customer’s Orders
Msgbox(targetCustomer.Orders.Contains(o))
' Displays 'True'.. order is detached from its customer
Msgbox(o.Customer = Nothing)
Comme pour toutes les autres modifications, la commande n’a pas été supprimée. Il nous semble simplement comme ça puisqu’il a été enlevé et détaché du reste de nos objets. Lorsque l’objet order a été supprimé de la table Orders , il a été marqué pour suppression par le service de suivi des modifications. La suppression effective de la base de données se produit lorsque les modifications sont envoyées lors d’un appel à SubmitChanges(). Notez que l’objet lui-même n’est jamais supprimé. Le runtime gère la durée de vie des instances d’objet, de sorte qu’il reste entier tant que vous conservez toujours une référence à celui-ci. Toutefois, une fois qu’un objet a été supprimé de sa table et que les modifications ont été envoyées, il n’est plus suivi par le service de suivi des modifications.
La seule autre fois qu’une entité est laissée sans suivi, c’est lorsqu’elle existe avant que le DataContext n’en soit conscient. Cela se produit chaque fois que vous créez de nouveaux objets dans votre code. Vous êtes libre d’utiliser des instances de classes d’entité dans votre application sans jamais les récupérer à partir d’une base de données. Les modifications de tacking et de gestion des identités s’appliquent uniquement aux objets dont le DataContext a connaissance. Par conséquent, aucun service n’est activé pour les instances nouvellement créées tant que vous ne les ajoutez pas au DataContext.
Cela peut se produire de l’une des deux façons suivantes. Vous pouvez appeler manuellement la méthode Add() sur la collection Table associée.
C#
Customer cust =
new Customer {
CustomerID = "ABCDE",
ContactName = "Frond Smooty",
CompanyTitle = "Eggbert's Eduware",
Phone = "888-925-6000"
};
// Add new customer to Customers table
db.Customers.Add(cust);
Visual Basic
Dim targetCustomer = New Customer With { _
.CustomerID = “ABCDE”, _
.ContactName = “Frond Smooty”, _
.CompanyTitle = “Eggbert’s Eduware”, _
.Phone = “888-925-6000”}
' Add new customer to Customers table
db.Customers.Add(cust)
Vous pouvez également attacher une nouvelle instance à un objet déjà pris en compte par le DataContext.
C#
// Add an order to a customer's Orders
cust.Orders.Add(
new Order { OrderDate = DateTime.Now }
);
Visual Basic
' Add an order to a customer's Orders
targetCustomer.Orders.Add( _
New Order With { .OrderDate = DateTime.Now } )
DataContext découvre vos nouvelles instances d’objet, même si elles sont attachées à d’autres nouvelles instances.
C#
// Add an order and details to a customer's Orders
Cust.Orders.Add(
new Order {
OrderDate = DateTime.Now,
OrderDetails = {
new OrderDetail {
Quantity = 1,
UnitPrice = 1.25M,
Product = someProduct
}
}
}
);
Visual Basic
' Add an order and details to a customer's Orders
targetCustomer.Orders.Add( _
New Order With { _
.OrderDate = DateTime.Now, _
.OrderDetails = New OrderDetail With { _
.Quantity = 1,
.UnitPrice = 1.25M,
.Product = someProduct
}
} )
Fondamentalement, le DataContext reconnaît toute entité dans votre graphique d’objet qui n’est actuellement pas suivie en tant que nouvelle instance, que vous ayez ou non appelé la méthode Add().
Utilisation d’un DataContext en lecture seule
De nombreux scénarios ne nécessitent pas la mise à jour des entités récupérées à partir de la base de données. L’affichage d’une table de Clients sur une page Web est un exemple évident. Dans tous ces cas, il est possible d’améliorer les performances en demandant au DataContext de ne pas suivre les modifications apportées aux entités. Pour ce faire, spécifiez la propriété ObjectTracking sur le DataContext comme dans le code suivant :
C#
db.ObjectTracking = false;
var q = db.Customers.Where( c => c.City = "London");
foreach(Customer c in q)
Display(c);
Visual Basic
db.ObjectTracking = False
Dim londonCustomers = From cust In db.Customer _
Where cust.City = "London"
For Each c in londonCustomers
Display(c)
Next
Envoi de modifications
Quel que soit le nombre de modifications que vous apportez à vos objets, ces modifications ont été apportées uniquement aux réplicas en mémoire. Rien n’est encore arrivé aux données réelles de la base de données. La transmission de ces informations au serveur n’aura pas lieu tant que vous n’en aurez pas explicitement fait la demande en appelant SubmitChanges() sur le DataContext.
C#
Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// make changes here
db.SubmitChanges();
Visual Basic
Dim db As New Northwind("c:\northwind\northwnd.mdf")
' make changes here
db.SubmitChanges()
Lorsque vous appelez SubmitChanges(),dataContext tente de traduire toutes vos modifications en commandes SQL équivalentes, en insérant, mettant à jour ou supprimant des lignes dans les tables correspondantes. Ces actions peuvent être remplacées par votre propre logique personnalisée si vous le souhaitez, mais l’ordre de soumission est orchestré par un service du DataContext appelé processeur de modifications.
La première chose qui se produit lorsque vous appelez SubmitChanges() est que l’ensemble des objets connus est examiné pour déterminer si de nouvelles instances y ont été attachées. Ces nouvelles instances sont ajoutées à l’ensemble d’objets suivis. Ensuite, tous les objets avec des modifications en attente sont classés dans une séquence d’objets en fonction des dépendances entre eux. Les objets dont les modifications dépendent d’autres objets sont séquencés après leurs dépendances. Les contraintes de clé étrangère et d’unicité dans la base de données jouent un rôle important dans la détermination de l’ordre correct des modifications. Ensuite, juste avant que les modifications réelles ne soient transmises, une transaction est démarrée pour encapsuler la série de commandes individuelles, sauf si l’une d’elles est déjà dans l’étendue. Enfin, une par une, les modifications apportées aux objets sont traduites en commandes SQL et envoyées au serveur.
À ce stade, toutes les erreurs détectées par la base de données entraînent l’abandon du processus de soumission et une exception est déclenchée. Toutes les modifications apportées à la base de données seront restaurées comme si aucune des soumissions n’avait jamais eu lieu. Le DataContext aura toujours un enregistrement complet de toutes les modifications. Il est donc possible de tenter de corriger le problème et de les renvoyer en appelant à nouveau SubmitChanges().
C#
Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// make changes here
try {
db.SubmitChanges();
}
catch (Exception e) {
// make some adjustments
...
// try again
db.SubmitChanges();
}
Visual Basic
Dim db As New Northwind("c:\northwind\northwnd.mdf")
' make changes here
Try
db.SubmitChanges()
Catch e As Exception
' make some adjustments
...
' try again
db.SubmitChanges()
End Try
Une fois la transaction autour de l’envoi terminée, le DataContext accepte les modifications apportées aux objets en oubliant simplement les informations de suivi des modifications.
Modifications simultanées
Il existe diverses raisons pour lesquelles un appel à SubmitChanges() peut échouer. Vous avez peut-être créé un objet avec une clé primaire non valide ; qui est déjà en cours d’utilisation, ou avec une valeur qui enfreint certaines contraintes case activée de la base de données. Ces types de vérifications sont difficiles à intégrer dans la logique métier, car ils nécessitent souvent une connaissance absolue de l’état complet de la base de données. Toutefois, la raison la plus probable de l’échec est simplement qu’une autre personne a apporté des modifications aux objets avant vous.
Cela serait certainement impossible si vous verrouillez chaque objet dans la base de données et utilisez une transaction entièrement sérialisée. Toutefois, ce style de programmation (concurrence pessimiste) est rarement utilisé, car il est coûteux et les véritables conflits se produisent rarement. La forme la plus courante de gestion des modifications simultanées consiste à utiliser une forme d’accès concurrentiel optimiste. Dans ce modèle, aucun verrou sur les lignes de la base de données n’est pris du tout. Cela signifie qu’un nombre quelconque de modifications apportées à la base de données a pu se produire entre le moment où vous avez récupéré vos objets pour la première fois et le moment où vous avez soumis vos modifications.
Par conséquent, à moins que vous ne souhaitiez utiliser une stratégie que la dernière mise à jour gagne, en effaçant tout ce qui s’est produit avant vous, vous souhaitez probablement être averti du fait que les données sous-jacentes ont été modifiées par quelqu’un d’autre.
DataContext offre une prise en charge intégrée de l’accès concurrentiel optimiste en détectant automatiquement les conflits de modification. Les mises à jour individuelles réussissent uniquement si l’état actuel de la base de données correspond à l’état dans lequel vous avez compris que les données doivent se trouver lorsque vous avez récupéré vos objets pour la première fois. Cela se produit sur une base par objet, vous alertant uniquement des violations si elles se produisent sur des objets auxquels vous avez apporté des modifications.
Vous pouvez contrôler le degré de détection des conflits de modification par dataContext lorsque vous définissez vos classes d’entité. Chaque attribut Column a une propriété appelée UpdateCheck qui peut être affectée à l’une des trois valeurs suivantes : Always, Never et WhenChanged. Si la valeur par défaut d’un attribut Column n’est pas définie sur Always, ce qui signifie que les valeurs de données représentées par ce membre sont toujours vérifiées pour les conflits, c’est-à-dire, sauf s’il existe un disjoncteur évident comme un tampon de version. Un attribut Column a une propriété IsVersion qui vous permet de spécifier si la valeur de données constitue un tampon de version géré par la base de données. Si une version existe, la version est utilisée seule pour déterminer si un conflit s’est produit.
Lorsqu’un conflit de modification se produit, une exception est levée comme s’il s’agissait d’une autre erreur. La transaction entourant l’envoi sera abandonnée, mais le DataContext restera le même, ce qui vous permettra de corriger le problème et de réessayer.
C#
while (retries < maxRetries) {
Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// fetch objects and make changes here
try {
db.SubmitChanges();
break;
}
catch (ChangeConflictException e) {
retries++;
}
}
Visual Basic
Do While retries < maxRetries
Dim db As New Northwind("c:\northwind\northwnd.mdf")
' fetch objects and make changes here
Try
db.SubmitChanges()
Exit Do
catch cce As ChangeConflictException
retries += 1
End Try
Loop
Si vous apportez des modifications sur un niveau intermédiaire ou un serveur, la chose la plus simple que vous pouvez faire pour corriger un conflit de modification consiste simplement à recommencer et à réessayer, en recréant le contexte et en réappliquer les modifications. D’autres options sont décrites dans la section suivante.
Transactions
Une transaction est un service fourni par des bases de données ou tout autre gestionnaire de ressources qui peut être utilisé pour garantir qu’une série d’actions individuelles se produisent automatiquement ; c’est-à-dire qu’ils réussissent tous ou qu’ils ne réussissent pas tous. Si ce n’est pas le cas, ils sont également automatiquement annulés avant que quoi que ce soit d’autre ne soit autorisé à se produire. Si aucune transaction n’est déjà dans l’étendue, dataContext démarre automatiquement une transaction de base de données pour protéger les mises à jour lorsque vous appelez SubmitChanges().
Vous pouvez choisir de contrôler le type de transaction utilisée, son niveau d’isolation ou ce qu’elle englobe réellement en la lançant vous-même. L’isolation de transaction que le DataContext utilisera est appelée ReadCommitted.
C#
Product prod = db.Products.Single(p => p.ProductID == 15);
if (prod.UnitsInStock > 0)
prod.UnitsInStock--;
using(TransactionScope ts = new TransactionScope()) {
db.SubmitChanges();
ts.Complete();
}
Visual Basic
Dim product = (From prod In db.Products _
Where prod.ProductID = 15).First
If product.UnitsInStock > 0) Then
product.UnitsInStock -= 1
End If
Using ts As TransactionScope = New TransactionScope())
db.SubmitChanges()
ts.Complete()
End Using
L’exemple ci-dessus lance une transaction entièrement sérialisée en créant un objet d’étendue de transaction. Toutes les commandes de base de données exécutées dans l’étendue de la transaction seront gardées par la transaction.
C#
Product prod = db.Products.Single(p => p.ProductId == 15);
if (prod.UnitsInStock > 0)
prod.UnitsInStock--;
using(TransactionScope ts = new TransactionScope()) {
db.ExecuteCommand("exec sp_BeforeSubmit");
db.SubmitChanges();
ts.Complete();
}
Visual Basic
Dim product = (From prod In db.Products _
Where prod.ProductID = 15).First
If product.UnitsInStock > 0) Then
product.UnitsInStock -= 1
End If
Using ts As TransactionScope = New TransactionScope())
db.ExecuteCommand(“exec sp_BeforeSubmit”)
db.SubmitChanges()
ts.Complete()
End Using
Cette version modifiée du même exemple utilise la méthode ExecuteCommand() sur le DataContext pour exécuter une procédure stockée dans la base de données juste avant l’envoi des modifications. Indépendamment de ce que la procédure stockée fait à la base de données, nous pouvons être certains que ses actions font partie de la même transaction.
Si la transaction se termine correctement, le DataContext supprime toutes les informations de suivi accumulées et traite les nouveaux états des entités comme inchangés. Toutefois, il ne restaure pas les modifications apportées à vos objets en cas d’échec de la transaction. Cela vous offre une flexibilité maximale dans la gestion des problèmes lors de la soumission des modifications.
Il est également possible d’utiliser une transaction SQL locale au lieu du nouveau TransactionScope. LINQ to SQL offre cette fonctionnalité pour vous aider à intégrer LINQ to SQL fonctionnalités dans des applications ADO.NET préexistantes. Cependant, si vous suivez cette route, vous devrez être responsable de beaucoup plus.
C#
Product prod = q.Single(p => p.ProductId == 15);
if (prod.UnitsInStock > 0)
prod.UnitsInStock--;
db.Transaction = db.Connection.BeginTransaction();
try {
db.SubmitChanges();
db.Transaction.Commit();
}
catch {
db.Transaction.Rollback();
throw;
}
finally {
db.Transaction = null;
}
Visual Basic
Dim product = (From prod In db.Products _
Where prod.ProductID = 15).First
If product.UnitsInStock > 0) Then
product.UnitsInStock -= 1
End If
db.Transaction = db.Connection.BeginTransaction()
Try
db.SubmitChanges()
db.Transaction.Commit()
catch e As Exception
db.Transaction.Rollback()
Throw e
Finally
db.Transaction = Nothing
End Try
Comme vous pouvez le voir, l’utilisation d’une transaction de base de données contrôlée manuellement est un peu plus impliquée. Non seulement devez-vous le démarrer vous-même, mais vous devez indiquer explicitement à DataContext de l’utiliser en l’affectant à la propriété Transaction . Vous devez ensuite utiliser un bloc try-catch pour enserrer votre logique d’envoi, en n’oubliez pas d’indiquer explicitement à la transaction de valider et de demander explicitement au DataContext d’accepter les modifications, ou d’abandonner les transactions en cas d’échec à un moment donné. En outre, n’oubliez pas de rebasser la propriété Transaction sur null lorsque vous avez terminé.
Procédures stockées
Lorsque SubmitChanges() est appelé, LINQ to SQL génère et exécute des commandes SQL pour insérer, mettre à jour et supprimer des lignes dans la base de données. Ces actions peuvent être remplacées par les développeurs d’applications et, à leur place, du code personnalisé peut être utilisé pour effectuer les actions souhaitées. De cette façon, d’autres fonctionnalités telles que les procédures stockées dans une base de données peuvent être appelées automatiquement par le processeur de modifications.
Envisagez une procédure stockée pour mettre à jour les unités en stock de la table Products dans l’exemple de base de données Northwind. La déclaration SQL de la procédure est la suivante.
SQL
create proc UpdateProductStock
@id int,
@originalUnits int,
@decrement int
as
Vous pouvez utiliser la procédure stockée au lieu de la commande normale de mise à jour générée automatiquement en définissant une méthode sur votre DataContext fortement typé. Même si la classe DataContext est générée automatiquement par l’outil de génération de code LINQ to SQL, vous pouvez toujours spécifier ces méthodes dans une classe partielle.
C#
public partial class Northwind : DataContext
{
...
public void UpdateProduct(Product original, Product current) {
// Execute the stored procedure for UnitsInStock update
if (original.UnitsInStock != current.UnitsInStock) {
int rowCount = this.ExecuteCommand(
"exec UpdateProductStock " +
"@id={0}, @originalUnits={1}, @decrement={2}",
original.ProductID,
original.UnitsInStock,
(original.UnitsInStock - current.UnitsInStock)
);
if (rowCount < 1)
throw new Exception("Error updating");
}
...
}
}
Visual Basic
Partial Public Class Northwind
Inherits DataContext
...
Public Sub UpdateProduct(original As Product, current As Product)
‘ Execute the stored procedure for UnitsInStock update
If original.UnitsInStock <> current.UnitsInStock Then
Dim rowCount As Integer = ExecuteCommand( _
"exec UpdateProductStock " & _
"@id={0}, @originalUnits={1}, @decrement={2}", _
original.ProductID, _
original.UnitsInStock, _
(original.UnitsInStock - current.UnitsInStock) )
If rowCount < 1 Then
Throw New Exception(“Error updating”)
End If
End If
...
End Sub
End Class
La signature de la méthode et le paramètre générique indiquent au DataContext d’utiliser cette méthode à la place d’une instruction de mise à jour générée. Les paramètres d’origine et actuel sont utilisés par LINQ to SQL pour transmettre les copies d’origine et actuelles de l’objet du type spécifié. Les deux paramètres sont disponibles pour la détection de conflits d’accès concurrentiel optimiste.
Note Si vous remplacez la logique de mise à jour par défaut, la détection des conflits est de votre responsabilité.
La procédure stockée UpdateProductStock est appelée à l’aide de la méthode ExecuteCommand() du DataContext. Elle retourne le nombre de lignes affectées et a la signature suivante :
C#
public int ExecuteCommand(string command, params object[] parameters);
Visual Basic
Public Function ExecuteCommand(command As String, _
ParamArray parameters() As Object) As Integer
Le tableau d’objets est utilisé pour passer les paramètres requis pour l’exécution de la commande.
À l’instar de la méthode update, les méthodes insert et delete peuvent être spécifiées. Les méthodes d’insertion et de suppression ne prennent qu’un seul paramètre du type d’entité à mettre à jour. Par exemple, les méthodes permettant d’insérer et de supprimer un instance Product peuvent être spécifiées comme suit :
C#
public void InsertProduct(Product prod) { ... }
public void DeleteProudct(Product prod) { ... }
Visual Basic
Public Sub InsertProduct(prod As Product) ...
Public Sub DeleteProudct(prod As Product) ...
Classes d’entités In-Depth
Utilisation d'attributs
Une classe d’entité est comme n’importe quelle classe d’objet normale que vous pouvez définir dans le cadre de votre application, sauf qu’elle est annotée avec des informations spéciales qui l’associent à une table de base de données particulière. Ces annotations sont effectuées en tant qu’attributs personnalisés sur votre déclaration de classe. Les attributs ne sont significatifs que lorsque vous utilisez la classe conjointement avec LINQ to SQL. Ils sont similaires aux attributs de sérialisation XML dans le .NET Framework. Ces attributs « data » fournissent aux LINQ to SQL suffisamment d’informations pour traduire les requêtes de vos objets en requêtes SQL sur la base de données et les modifications apportées à vos objets en commandes d’insertion, de mise à jour et de suppression SQL.
Il est également possible de représenter les informations de mappage à l’aide d’un fichier de mappage XML au lieu d’attributs. Ce scénario est décrit plus en détail dans la section Mappage externe.
Attribut Database
L’attribut Database est utilisé pour spécifier le nom par défaut de la base de données s’il n’est pas fourni par la connexion. Les attributs de base de données peuvent être appliqués aux déclarations DataContext fortement typées. Cet attribut est facultatif.
Attribut Database
Propriété | Type | Description |
---|---|---|
Nom | String | Spécifie le nom de la base de données. Les informations sont utilisées uniquement si la connexion elle-même ne spécifie pas le nom de la base de données. Si cet attribut Database n’existe pas dans la déclaration de contexte et qu’aucun attribut n’est spécifié par la connexion, la base de données est supposée avoir le même nom que la classe de contexte. |
C#
[Database(Name="Database#5")]
public class Database5 : DataContext {
...
}
Visual Basic
<Database(Name:="Database#5")> _
Public Class Database5
Inherits DataContext
...
End Class
Attribut de table
L’attribut Table est utilisé pour désigner une classe en tant que classe d’entité associée à une table de base de données. Les classes avec l’attribut Table sont traitées spécialement par LINQ to SQL.
Attribut de table
Propriété | Type | Description |
---|---|---|
Nom | String | Spécifie le nom de la table. Si ces informations ne sont pas spécifiées, il est supposé que la table porte le même nom que la classe d’entité. |
C#
[Table(Name="Customers")]
public class Customer {
...
}
Visual Basic
<Table(Name:="Customers")> _
Public Class Customer
...
End Class
Attribut de colonne
L’attribut Column est utilisé pour désigner un membre d’une classe d’entité qui représente une colonne dans une table de base de données. Il peut être appliqué à n’importe quel champ ou propriété, public, privé ou interne. Seuls les membres identifiés en tant que colonnes sont conservés lorsque LINQ to SQL enregistre les modifications apportées à la base de données.
Attribut de colonne
Propriété | Type | Description |
---|---|---|
Nom | String | Nom de la colonne dans la table ou la vue. Si elle n’est pas spécifiée, la colonne est supposée avoir le même nom que le membre de classe. |
Stockage | String | Nom du stockage sous-jacent. Si elle est spécifiée, elle indique LINQ to SQL comment contourner l’accesseur de propriété publique pour le membre de données et interagir avec la valeur brute elle-même. S’il n’est pas spécifié LINQ to SQL obtient et définit la valeur à l’aide de l’accesseur public. |
DBType | String | Type de colonne de base de données spécifié à l’aide des types et modificateurs de base de données. Il s’agit du texte exact utilisé pour définir la colonne dans une commande de déclaration de table T-SQL. S’il n’est pas spécifié, le type de colonne de base de données est déduit du type membre. Le type de base de données spécifique n’est nécessaire que si la méthode CreateDatabase() est censée être utilisée pour créer une instance de la base de données. |
IsPrimaryKey | Bool | Si la valeur est true, le membre de classe représente une colonne qui fait partie de la clé primaire de la table. Si plusieurs membres de la classe sont désignés comme ID, la clé primaire est dite composite des colonnes associées. |
IsDbGenerated | Boolean | Identifie que la valeur de colonne du membre est générée automatiquement par la base de données. Les clés primaires désignées IsDbGenerated=true doivent également avoir un DBType avec le modificateur IDENTITY .
IsDbGenerated les membres sont synchronisés immédiatement après l’insertion de la ligne de données et sont disponibles une fois SubmitChanges() terminé. |
IsVersion | Boolean | Identifie le type de colonne du membre en tant qu’horodatage de base de données ou numéro de version. Les numéros de version sont incrémentés et les colonnes timestamp sont mises à jour par la base de données chaque fois que la ligne associée est mise à jour. Les membres avec IsVersion=true sont synchronisés immédiatement après la mise à jour de la ligne de données. Les nouvelles valeurs sont visibles une fois SubmitChanges() terminé. |
UpdateCheck | UpdateCheck | Détermine comment LINQ to SQL implémente la détection des conflits d’accès concurrentiel optimiste. Si aucun membre n’est désigné comme IsVersion=true, la détection est effectuée en comparant les valeurs de membre d’origine à l’état actuel de la base de données. Vous pouvez contrôler les membres que LINQ to SQL utilise lors de la détection des conflits en donnant à chaque membre une valeur d’énumération UpdateCheck.
|
IsDiscriminator | Boolean | Détermine si le membre de classe détient la valeur de discriminateur pour une hiérarchie d’héritage. |
Expression | String | N’affecte pas le fonctionnement de LINQ to SQL, mais est utilisé pendant .CreateDatabase() en tant qu’expression SQL brute représentant l’expression de colonne calculée. |
CanBeNull | Boolean | Indique que la valeur peut contenir la valeur Null. Cela est généralement déduit du type CLR du membre d’entité. Utilisez cet attribut pour indiquer qu’une valeur de chaîne est représentée sous la forme d’une colonne non nullable dans la base de données. |
AutoSync | AutoSync | Spécifie si la colonne est automatiquement synchronisée à partir de la valeur générée par la base de données sur les commandes d’insertion ou de mise à jour. Les valeurs valides pour cette balise sont OnInsert, Always et Never. |
Une classe d’entité classique utilise les attributs Column sur les propriétés publiques et stocke les valeurs réelles dans les champs privés.
C#
private string _city;
[Column(Storage="_city", DBType="NVarChar(15)")]
public string City {
get { ... }
set { ... }
}
Visual Basic
Private _city As String
<Column(Storage:="_city", DBType:="NVarChar(15)")> _
public Property City As String
Get
set
End Property
Le DBType est spécifié uniquement afin que la méthode CreateDatabase() puisse construire la table avec le type le plus précis. Sinon, le fait de savoir que la colonne sous-jacente est limitée à 15 caractères n’est pas utilisé.
Les membres représentant la clé primaire d’un type de base de données sont souvent associés à des valeurs générées automatiquement.
C#
private string _orderId;
[Column(Storage="_orderId", IsPrimaryKey=true, IsDbGenerated = true,
DBType="int NOT NULL IDENTITY")]
public string OrderId {
get { ... }
set { ... }
}
Visual Basic
Private _orderId As String
<Column(Storage:="_orderId", IsPrimaryKey:=true, _
IsDbGenerated:= true, DBType:="int NOT NULL IDENTITY")> _
public Property OrderId As String
Get
Set
End Property
Si vous spécifiez le DBType, veillez à inclure le modificateur IDENTITY . LINQ to SQL n’augmente pas un DBType spécifié personnalisé. Toutefois, si le DBType n’est pas spécifié LINQ to SQL déduit que le modificateur IDENTITY est nécessaire lors de la création de la base de données via la méthode CreateDatabase().
De même, si la propriété IsVersion a la valeur true, dbType doit spécifier les modificateurs appropriés pour désigner un numéro de version ou une colonne timestamp. Si aucun DBType n’est spécifié, LINQ to SQL déduit les modificateurs corrects.
Vous pouvez contrôler l’accès à un membre associé à une colonne générée automatiquement, à un tampon de version ou à toute colonne que vous souhaiterez peut-être masquer en désignant le niveau d’accès du membre, ou même en limitant l’accesseur lui-même.
C#
private string _customerId;
[Column(Storage="_customerId", DBType="NCHAR(5) ")]
public string CustomerID {
get { ... }
}
Visual Basic
Private _customerId As String
<Column(Storage:="_customerId", DBType:="NCHAR(5)")> _
Public Property CustomerID As String
Get
End Property
La propriété CustomerID de la commande peut être rendue en lecture seule en ne définissant pas d’accesseur défini. LINQ to SQL pouvez toujours obtenir et définir la valeur sous-jacente via le membre de stockage.
Vous pouvez également rendre un membre complètement inaccessible au reste de l’application en plaçant un attribut Column sur un membre privé. Cela permet à la classe d’entité de contenir des informations pertinentes pour la logique métier de la classe sans les exposer en général. Même si les membres privés font partie des données traduites, étant donné qu’ils sont privés, vous ne pouvez pas y faire référence dans une requête intégrée au langage.
Par défaut, tous les membres sont utilisés pour effectuer une détection optimiste des conflits d’accès concurrentiel. Vous pouvez contrôler si un membre particulier est utilisé en spécifiant sa valeur UpdateCheck .
C#
[Column(Storage="_city", UpdateCheck=UpdateCheck.WhenChanged)]
public string City {
get { ... }
set { ... }
}
Visual Basic
<Column(Storage:="_city", UpdateCheck:=UpdateCheck.WhenChanged)> _
Public Property City As String
Get
Set
End Property
Le tableau suivant montre les mappages autorisés entre les types de base de données et le type CLR correspondant. Utilisez ce tableau comme guide pour déterminer le type CLR à utiliser pour représenter une colonne de base de données particulière.
Mappages autorisés du type de base de données et du type CLR correspondant
Type de base de données | Type CLR .NET | Commentaires |
---|---|---|
bit, tinyint, smallint, int, bigint | Bye, Int16, Uint16, Int32, Uint32, Int64, Uint64 | Conversions avec perte possible. Les valeurs ne peuvent pas aller-retour. |
bit | Boolean | |
decimal, numeric, smallmoney, money | Decimal | La différence d’échelle peut entraîner une conversion avec perte. Peut ne pas aller-retour. |
real, float | Single, Double | Différences de précision. |
char, varchar, text, nchar, nvarchar, ntext | String | Différences de paramètres régionaux possibles. |
datetime, smalldatetime | DateTime | Une précision différente peut entraîner des problèmes de conversion avec perte et d’aller-retour. |
UNIQUEIDENTIFIER | Guid | Règles de classement différentes. Le tri peut ne pas fonctionner comme prévu. |
timestamp | Byte[] (Byte() en Visual Basic), Binaire | Le tableau d’octets est traité comme un type scalaire. L’utilisateur est responsable de l’allocation d’un stockage adéquat lorsque le constructeur est appelé. Il est considéré comme immuable et n’est pas suivi des modifications. |
binary, varbinary | Byte[] (Byte() en Visual Basic), Binaire |
Attribut d’association
L’attribut Association est utilisé pour désigner une propriété qui représente une association de base de données comme une relation clé étrangère à clé primaire.
Attribut d’association
Propriété | Type | Description |
---|---|---|
Nom | String | Nom de l'association. Il s’agit souvent du nom de la contrainte de clé étrangère de la base de données. Il est utilisé lorsque CreateDatabase() est utilisé pour créer une instance de la base de données afin de générer la contrainte appropriée. Il est également utilisé pour faire la distinction entre plusieurs relations dans une classe d’entité unique faisant référence à la même classe d’entité cible. Dans ce cas, les propriétés de relation situées sur les côtés de la relation (si les deux sont définies) doivent avoir le même nom. |
Stockage | String | Nom du membre de stockage sous-jacent. Si elle est spécifiée, elle indique LINQ to SQL comment contourner l’accesseur de propriété publique pour le membre de données et interagir avec la valeur brute elle-même. S’il n’est pas spécifié LINQ to SQL obtient et définit la valeur à l’aide de l’accesseur public. Il est recommandé que tous les membres de l’association soient des propriétés avec des membres de stockage distincts identifiés. |
ThisKey | String | Liste séparée par des virgules des noms d’un ou de plusieurs membres de cette classe d’entité qui représentent les valeurs de clé de ce côté de l’association. S’ils ne sont pas spécifiés, les membres sont supposés être les membres qui composent la clé primaire. |
OtherKey | String | Liste séparée par des virgules des noms d’un ou de plusieurs membres de la classe d’entité cible qui représentent les valeurs de clé de l’autre côté de l’association. S’ils ne sont pas spécifiés, les membres sont supposés être les membres qui composent la clé primaire de l’autre classe d’entité. |
IsUnique | Boolean | True s’il existe une contrainte d’unicité sur la clé étrangère, indiquant une relation 1:1 vraie. Cette propriété est rarement utilisée, car les relations 1:1 sont presque impossibles à gérer dans la base de données. La plupart du temps, les modèles d’entité sont définis à l’aide de relations 1:n, même quand ils sont traités comme 1:1 par les développeurs d’applications. |
IsForeignKey | Boolean | True si le type « other » cible de l’association est le parent du type source. Avec les relations de clé étrangère à clé primaire, le côté qui contient la clé étrangère est l’enfant et le côté contenant la clé primaire est le parent. |
DeleteRule | String | Permet d’ajouter le comportement de suppression à cette association. Par exemple, « CASCADE » ajoute « ON DELETE CASCADE » à la relation FK. Si la valeur est null, aucun comportement de suppression n’est ajouté. |
Les propriétés d’association représentent une référence unique à une autre classe d’entité instance ou elles représentent une collection de références. Les références Singleton doivent être encodées dans la classe d’entité à l’aide du type de valeur EntityRef<T>(EntityRef (OfT) en Visual Basic) pour stocker la référence réelle. Le type EntityRef permet LINQ to SQL chargement différé des références.
C#
class Order
{
...
private EntityRef<Customer> _Customer;
[Association(Name="FK_Orders_Customers", Storage="_Customer",
ThisKey="CustomerID")]
public Customer Customer {
get { return this._Customer.Entity; }
set { this._Customer.Entity = value;
// Additional code to manage changes }
}
}
Visual Basic
Class Order
...
Private _customer As EntityRef(Of Customer)
<Association(Name:="FK_Orders_Customers", _
Storage:="_Customer", ThisKey:="CustomerID")> _
public Property Customer() As Customer
Get
Return _customer.Entity
End Get
Set (value As Customer)
_customer.Entity = value
‘ Additional code to manage changes
End Set
End Class
La propriété publique est tapée comme Customer, et non EntityRef<Customer>. Il est important de ne pas exposer le type EntityRef dans le cadre de l’API publique, car les références à ce type dans une requête ne seront pas traduites en SQL.
De même, une propriété d’association représentant une collection doit utiliser le type de collection EntitySet<T> (EntitySet(OfT) en Visual Basic) pour stocker la relation.
C#
class Customer
{
...
private EntitySet<Order> _Orders;
[Association(Name="FK_Orders_Customers", Storage="_Orders",
OtherKey="CustomerID")]
public EntitySet<Order> Orders {
get { return this._Orders; }
set { this._Orders.Assign(value); }
}
}
Visual Basic
Class Customer
...
Private _Orders As EntitySet(Of Order)
<Association(Name:="FK_Orders_Customers", _
Storage:="_Orders", OtherKey:="CustomerID")> _
public Property Orders() As EntitySet(Of Order)
Get
Return _Orders
End Get
Set (value As EntitySet(Of Order))
_Orders.Assign(value)
End Property
End Class
Toutefois, étant donné qu’un EntitySet<T> (EntitySet(OfT) en Visual Basic) est une collection, il est valide d’utiliser EntitySet comme type de retour. Il est également valide pour masquer le type réel de la collection, en utilisant plutôt l’interface ICollection<T> (ICollection(OfT) en Visual Basic.
C#
class Customer
{
...
private EntitySet<Order> _Orders;
[Association(Name="FK_Orders_Customers", Storage="_Orders",
OtherKey="CustomerID")]
public ICollection<Order> Orders {
get { return this._Orders; }
set { this._Orders.Assign(value); }
}
}
Visual Basic
Class Customer
...
Private _orders As EntitySet(Of Order)
<Association(Name:="FK_Orders_Customers", _
Storage:="_Orders", OtherKey:="CustomerID")> _
public Property Orders() As ICollection (Of Order)
Get
Return _orders
End Get
Set (value As ICollection (Of Order))
_orders.Assign(value)
End Property
End Class
Veillez à utiliser la méthode Assign() sur EntitySet si vous exposez un setter public pour la propriété . Cela permet à la classe d’entité de continuer à utiliser la même collection instance, car elle peut déjà être liée au service de suivi des modifications.
Attribut ResultType
Cet attribut spécifie un type d’élément d’une séquence énumérable qui peut être retournée à partir d’une fonction qui a été déclarée pour retourner l’interface IMultipleResults . Cet attribut peut être spécifié plusieurs fois.
Attribut ResultType
Propriété | Type | Description |
---|---|---|
Type | Type | Type des résultats retournés. |
StoredProcedure, attribut
L’attribut StoredProcedure est utilisé pour déclarer qu’un appel à une méthode définie sur le type DataContext ou Schema est traduit en tant qu’appel à une procédure stockée de base de données.
StoredProcedure, attribut
Propriété | Type | Description |
---|---|---|
Nom | String | Nom de la procédure stockée dans la base de données. Si elle n’est pas spécifiée, la procédure stockée est supposée avoir le même nom que la méthode |
Attribut de fonction
L’attribut Function est utilisé pour déclarer qu’un appel à une méthode définie sur un DataContext ou schema est traduit en appel à une fonction scalaire définie par l’utilisateur ou table de base de données.
Attribut de fonction
Propriété | Type | Description |
---|---|---|
Nom | String | Nom de la fonction dans la base de données. Si elle n’est pas spécifiée, la fonction est supposée avoir le même nom que la méthode |
Attribut de paramètre
L’attribut Parameter est utilisé pour déclarer un mappage entre une méthode et les paramètres d’une procédure stockée de base de données ou d’une fonction définie par l’utilisateur.
Attribut de paramètre
Propriété | Type | Description |
---|---|---|
Nom | String | Nom du paramètre dans la base de données. S’il n’est pas spécifié, le paramètre est déduit du nom du paramètre de méthode. |
DBType | String | Type de paramètre spécifié à l’aide de types de base de données et de modificateurs. |
InheritanceMapping Attribute
L’attribut InheritanceMapping est utilisé pour décrire la correspondance entre un code discriminateur particulier et un sous-type d’héritage. Tous les attributs InheritanceMapping utilisés pour une hiérarchie d’héritage doivent être déclarés sur le type racine de la hiérarchie.
InheritanceMapping Attribute
Propety | Type | Description |
---|---|---|
Code | Object | Valeur de code du discriminateur. |
Type | Type | Sous-type Héritage. Il peut s’agir de n’importe quel type non abstrait dans la hiérarchie d’héritage, y compris le type racine. |
IsDefault | Boolean | Détermine si le sous-type d’héritage spécifié est le type par défaut construit quand LINQ to SQL trouve un code de discriminateur qui n’est pas défini par les attributs InheritanceMapping. Exactement l’un des attributs InheritanceMapping doit être déclaré avec IsDefault comme true. |
Cohérence des graphiques
Un graphe est un terme général pour une structure de données d’objets qui se font tous références les uns aux autres par des références. Une hiérarchie (ou arborescence) est une forme dégénérée de graphe. Les modèles objet spécifiques à un domaine décrivent souvent un réseau de références qui sont mieux décrites comme un graphique d’objets. L’intégrité de votre graphe d’objets est d’une importance vitale pour la stabilité de votre application. C’est pourquoi il est important de s’assurer que les références dans le graphique restent cohérentes avec vos règles d’entreprise et/ou vos contraintes définies dans la base de données.
LINQ to SQL ne gère pas automatiquement la cohérence des références de relation pour vous. Lorsque les relations sont bidirectionnelles, une modification d’un côté de la relation doit automatiquement mettre à jour l’autre. Notez qu’il est rare que les objets normaux se comportent de cette façon. Il est donc peu probable que vous ayez conçu vos objets de cette façon dans le cas contraire.
LINQ to SQL fournit quelques mécanismes pour faciliter ce travail et un modèle à suivre pour vous assurer que vous gérez correctement vos références. Les classes d’entité générées par l’outil de génération de code implémentent automatiquement les modèles appropriés.
C#
public class Customer() {
this._Orders =
new EntitySet<Order>(
new Action<Order>(this.attach_Orders),
new Action<Order>(this.detach_Orders));
);}
Visual Basic
Public Class Customer()
_Orders = New EntitySet(Of Order)( _
New Action(Of Order)(attach_Orders), _
New Action(Of Order)(detach_Orders))
End Class
);}
Le type EntitySet<T> (EntitySet(OfT) en Visual Basic) a un constructeur qui vous permet de fournir deux délégués à utiliser comme rappels : le premier lorsqu’un élément est ajouté à la collection, le second lorsqu’il est supprimé. Comme vous pouvez le voir dans l’exemple, le code que vous spécifiez pour ces délégués peut et doit être écrit pour mettre à jour la propriété de relation inverse. C’est ainsi que la propriété Customer d’un instance de commande est automatiquement modifiée lorsqu’une commande est ajoutée à la collection Commandes d’un client.
L’implémentation de la relation à l’autre extrémité n’est pas aussi facile. EntityRef<T> (EntityRef(OfT) en Visual Basic) est un type valeur défini pour contenir aussi peu de surcharge supplémentaire que possible par rapport à la référence d’objet réelle. Il n’a pas de place pour une paire de délégués. Au lieu de cela, le code qui gère la cohérence des graphes des références singleton doit être incorporé dans les accesseurs de propriété eux-mêmes.
C#
[Association(Name="FK_Orders_Customers", Storage="_Customer",
ThisKey="CustomerID")]
public Customer Customer {
get {
return this._Customer.Entity;
}
set {
Customer v = this._Customer.Entity;
if (v != value) {
if (v != null) {
this._Customer.Entity = null;
v.Orders.Remove(this);
}
this._Customer.Entity = value;
if (value != null) {
value.Orders.Add(this);
}
}
}
}
Visual Basic
<Association(Name:="FK_Orders_Customers", _
Storage:="_Customer", ThisKey:="CustomerID")> _
Public Property Customer As Customer
Get
Return _Customer.Entity
End Get
Set (value As Customer)
Dim cust As Customer v = _customer.Entity
if cust IsNot value Then
If cust IsNot Nothing Then
_Customer.Entity = Nothing
cust.Orders.Remove(Me)
End If
_customer.Entity = value
if value IsNot Nothing Then
value.Orders.Add(Me)
End If
End If
End Set
End Property
Jetez un coup d’œil au setter. Lorsque la propriété Customer est modifiée, le instance de commande est d’abord supprimé de la collection Commandes du client actuel, puis ajouté uniquement ultérieurement à la collection du nouveau client. Notez qu’avant que l’appel à Remove() ne soit effectué, la référence d’entité réelle est définie sur null. Cela permet d’éviter la récursivité lorsque la méthode Remove() est appelée. N’oubliez pas que EntitySet utilisera des délégués de rappel pour affecter la propriété Customer de cet objet à null. La même chose se produit juste avant l’appel à Add(). La référence d’entité réelle est mise à jour vers la nouvelle valeur. Cela réduira à nouveau toute récursivité potentielle et, bien sûr, accomplira la tâche du setter en premier lieu.
La définition d’une relation un-à-un est très similaire à la définition d’une relation un-à-plusieurs du côté de la référence singleton. Au lieu d’appeler Add()
et Remove(), un nouvel objet est affecté ou un objet null est affecté pour rompre la relation.
Là encore, il est essentiel que les propriétés de relation conservent la cohérence du graphe d’objets. Si le graphe d’objets en mémoire n’est pas cohérent avec les données de base de données, une exception d’exécution est générée lorsque la méthode SubmitChanges est appelée. Envisagez d’utiliser l’outil de génération de code pour maintenir le travail de cohérence pour vous.
Notifications de modification
Vos objets peuvent participer au processus de suivi des modifications. Il n’est pas obligatoire qu’ils le fassent, mais ils peuvent considérablement réduire la quantité de surcharge nécessaire pour suivre les modifications potentielles des objets. Il est probable que votre application récupérera beaucoup plus d’objets à partir de requêtes que ne finira par être modifié. Sans aide proactive de vos objets, le service de suivi des modifications est limité dans la façon dont il peut réellement suivre les modifications.
Étant donné qu’il n’existe aucun vrai service d’interception dans le runtime, le suivi formel ne se produit pas réellement. Au lieu de cela, les copies en double des objets sont stockées lors de leur première récupération. Plus tard, lorsque vous appelez SubmitChanges(), ces copies sont utilisées pour comparer celles qui vous ont été données. Si leurs valeurs diffèrent, l’objet a été modifié. Cela signifie que chaque objet nécessite deux copies en mémoire, même si vous ne les modifiez jamais.
Une meilleure solution consiste à faire annoncer les objets eux-mêmes au service de suivi des modifications lorsqu’ils sont effectivement modifiés. Pour ce faire, l’objet implémente une interface qui expose un événement de rappel. Le service de suivi des modifications peut ensuite connecter chaque objet et recevoir des notifications en cas de modification.
C#
[Table(Name="Customers")]
public partial class Customer: INotifyPropertyChanging {
public event PropertyChangingEventHandler PropertyChanging;
private void OnPropertyChanging() {
if (this.PropertyChanging != null) {
this.PropertyChanging(this, emptyEventArgs);
}
}
private string _CustomerID;
[Column(Storage="_CustomerID", IsPrimaryKey=true)]
public string CustomerID {
get {
return this._CustomerID;
}
set {
if ((this._CustomerID != value)) {
this.OnPropertyChanging("CustomerID");
this._CustomerID = value;
}
}
}
}
Visual Basic
<Table(Name:="Customers")> _
Partial Public Class Customer
Inherits INotifyPropertyChanging
Public Event PropertyChanging As PropertyChangingEventHandler _
Implements INotifyPropertyChanging.PropertyChanging
Private Sub OnPropertyChanging()
RaiseEvent PropertyChanging(Me, emptyEventArgs)
End Sub
private _customerID As String
<Column(Storage:="_CustomerID", IsPrimaryKey:=True)>
public Property CustomerID() As String
Get
Return_customerID
End Get
Set (value As Customer)
If _customerID IsNot value Then
OnPropertyChanging(“CustomerID”)
_CustomerID = value
End IF
End Set
End Function
End Class
Pour améliorer le suivi des modifications, vos classes d’entité doivent implémenter l’interface INotifyPropertyChanging . Il vous faut uniquement définir un événement appelé PropertyChanging. Le service de suivi des modifications s’inscrit ensuite auprès de votre événement lorsque vos objets entrent en sa possession. Tout ce que vous devez faire est de déclencher cet événement immédiatement avant que vous ne soyez sur le point de modifier la valeur d’une propriété.
N’oubliez pas également de placer la même logique de déclenchement d’événements dans vos setters de propriétés de relation. Pour EntitySets, déclenchez les événements dans les délégués que vous fournissez.
C#
public Customer() {
this._Orders =
new EntitySet<Order>(
delegate(Order entity) {
this.OnPropertyChanging("Orders");
entity.Customer = this;
},
delegate(Order entity) {
this.onPropertyChanging("Orders");
entity.Customer = null;
}
);
}
Visual Basic
Dim _orders As EntitySet(Of Order)
Public Sub New()
_orders = New EntitySet(Of Order)( _
AddressOf OrderAdding, AddressOf OrderRemoving)
End Sub
Sub OrderAdding(ByVal o As Order)
OnPropertyChanging()
o.Customer = Me
End Sub
Sub OrderRemoving(ByVal o As Order)
OnPropertyChanging()
o.Customer = Nothing
End Sub
Héritage
LINQ to SQL prend en charge le mappage à table unique, par lequel une hiérarchie d’héritage entière est stockée dans une table de base de données unique. La table contient l’union aplatie de toutes les colonnes de données possibles pour l’ensemble de la hiérarchie et chaque ligne a des valeurs null dans les colonnes qui ne s’appliquent pas au type du instance représenté par la ligne. La stratégie de mappage de table unique est la représentation la plus simple de l'héritage. Elle fournit des caractéristiques de performances satisfaisantes pour de nombreuses catégories de requêtes.
Mappage
Pour implémenter ce mappage à l’aide de LINQ to SQL, vous devez spécifier les attributs et propriétés d’attribut suivants sur la classe racine de la hiérarchie d’héritage :
- Attribut [Table] (<Table> en Visual Basic).
- Attribut [HéritageMapping] (<HéritageMapping> en Visual Basic) pour chaque classe de la structure hiérarchique. Pour les classes non abstraites, cet attribut doit définir une propriété Code (une valeur qui apparaît dans la table de base de données de la colonne Discrimination d’héritage pour indiquer à quelle classe ou sous-classe cette ligne de données appartient) et une propriété Type (qui spécifie la classe ou la sous-classe que la valeur de clé signifie).
- Propriété IsDefault sur un seul attribut [InheritanceMapping] (<InheritanceMapping> en Visual Basic). Cette propriété sert à désigner un mappage « de secours » au cas où la valeur du discriminateur de la table de base de données ne correspond à aucune des valeurs de code dans les mappages d’héritage.
- Propriété IsDiscriminator pour un attribut [Column] (<Column> in Visual Basic), pour indiquer qu’il s’agit de la colonne qui contient la valeur code pour le mappage d’héritage.
Les sous-classes ne requièrent pas de propriétés ni d'attributs spéciaux. Notez en particulier que les sous-classes n’ont pas l’attribut [Table] (<Table> en Visual Basic).
Dans l’exemple suivant, les données contenues dans les sous-classes Car et Truck sont mappées à la table de base de données unique Vehicle. (Pour simplifier l’exemple, l’exemple de code utilise des champs plutôt que des propriétés pour le mappage de colonnes.)
C#
[Table]
[InheritanceMapping(Code = "C", Type = typeof(Car))]
[InheritanceMapping(Code = "T", Type = typeof(Truck))]
[InheritanceMapping(Code = "V", Type = typeof(Vehicle),
IsDefault = true)]
public class Vehicle
{
[Column(IsDiscriminator = true)]
public string Key;
[Column(IsPrimaryKey = true)]
public string VIN;
[Column]
public string MfgPlant;
}
public class Car : Vehicle
{
[Column]
public int TrimCode;
[Column]
public string ModelName;
}
public class Truck : Vehicle
{
[Column]
public int Tonnage;
[Column]
public int Axles;
}
Visual Basic
<Table> _
<InheritanceMapping(Code:="C", Type:=Typeof(Car))> _
<InheritanceMapping(Code:="T", Type:=Typeof(Truck))> _
<InheritanceMapping(Code:="V", Type:=Typeof(Vehicle), _
IsDefault:=true)> _
Public Class Vehicle
<Column(IsDiscriminator:=True)> _
Public Key As String
<Column(IsPrimaryKey:=True)> _
Public VIN As String
<Column> _
Public MfgPlant As String
End Class
Public Class Car
Inherits Vehicle
<Column> _
Public TrimCode As Integer
<Column> _
Public ModelName As String
End Class
Public class Truck
Inherits Vehicle
<Column> _
public Tonnage As Integer
<Column> _
public Axles As Integer
End Class
Le diagramme de classes s’affiche comme suit :
Figure 1. Diagramme de classe de véhicule
Lorsque vous affichez le diagramme de base de données résultant dans Server Explorer, vous voyez que les colonnes ont toutes été mappées à une seule table, comme illustré ici :
Figure 2 : Colonnes mappées à une table unique
Notez que les types des colonnes qui représentent des champs dans les sous-types doivent être nullables ou doivent avoir une valeur par défaut spécifiée. Cela est nécessaire pour que les commandes d’insertion réussissent.
Interrogation
Le code suivant fournit une description de la façon dont vous pouvez utiliser des types dérivés dans vos requêtes :
C#
var q = db.Vehicle.Where(p => p is Truck);
//or
var q = db.Vehicle.OfType<Truck>();
//or
var q = db.Vehicle.Select(p => p as Truck).Where(p => p != null);
foreach (Truck p in q)
Console.WriteLine(p.Axles);
Visual Basic
Dim trucks = From veh In db.Vehicle _
Where TypeOf(veh) Is Truck
For Each truck In trucks
Console.WriteLine(p.Axles)
Next
Avancé
Vous pouvez développer une hiérarchie bien au-delà de l’exemple simple déjà fourni.
Exemple 1
Voici une hiérarchie beaucoup plus profonde et une requête plus complexe :
C#
[Table]
[InheritanceMapping(Code = "V", Type = typeof(Vehicle), IsDefault = true)]
[InheritanceMapping(Code = "C", Type = typeof(Car))]
[InheritanceMapping(Code = "T", Type = typeof(Truck))]
[InheritanceMapping(Code = "S", Type = typeof(Semi))]
[InheritanceMapping(Code = "D", Type = typeof(DumpTruck))]
public class Truck: Vehicle { ... }
public class Semi: Truck { ... }
public class DumpTruck: Truck { ... }
...
// Get all trucks along with a flag indicating industrial application.
db.Vehicles.OfType<Truck>.Select(t =>
new {Truck=t, IsIndustrial=t is Semi || t is DumpTruck }
);
Visual Basic
<Table> _
<InheritanceMapping(Code:="V", Type:=Typeof(Vehicle), IsDefault:=True)> _
<InheritanceMapping(Code:="C", Type:=Typeof(Car))> _
<InheritanceMapping(Code:="T", Type:=Typeof(Truck))> _
<InheritanceMapping(Code:="S", Type:=Typeof(Semi))> _
<InheritanceMapping(Code:="D", Type:=Typeof(DumpTruck))> _
Public Class Truck
InheritsVehicle
Public Class Semi
Inherits Truck
Public Class DumpTruck
InheritsTruck
...
' Get all trucks along with a flag indicating industrial application.
Dim trucks = From veh In db.Vehicle _
Where Typeof(veh) Is Truck And _
IsIndustrial = (Typeof(veh) Is Semi _
Or Typeof(veh) Is DumpTruck)
Exemple 2
La hiérarchie suivante comprend des interfaces :
C#
[Table]
[InheritanceMapping(Code = "V", Type = typeof(Vehicle),
IsDefault = true)]
[InheritanceMapping(Code = "C", Type = typeof(Car))]
[InheritanceMapping(Code = "T", Type = typeof(Truck))]
[InheritanceMapping(Code = "S", Type = typeof(Semi))]
[InheritanceMapping(Code = "H", Type = typeof(Helicopter))]
public class Truck: Vehicle
public class Semi: Truck, IRentableVehicle
public class Helicopter: Vehicle, IRentableVehicle
Visual Basic
<Table> _
<InheritanceMapping(Code:="V", Type:=TypeOf(Vehicle),
IsDefault:=True) > _
<InheritanceMapping(Code:="C", Type:=TypeOf(Car)) > _
<InheritanceMapping(Code:="T", Type:=TypeOf(Truck)) > _
<InheritanceMapping(Code:="S", Type:=TypeOf(Semi)) > _
<InheritanceMapping(Code:="H", Type:=TypeOf(Helicopter)) > _
Public Class Truck
Inherits Vehicle
Public Class Semi
InheritsTruck, IRentableVehicle
Public Class Helicopter
InheritsVehicle, IRentableVehicle
Les requêtes possibles sont les suivantes :
C#
// Get commercial vehicles ordered by cost to rent.
db.Vehicles.OfType<IRentableVehicle>.OrderBy(cv => cv.RentalRate);
// Get all non-rentable vehicles
db.Vehicles.Where(v => !(v is IRentableVehicle));
Visual Basic
' Get commercial vehicles ordered by cost to rent.
Dim rentableVehicles = From veh In _
db.Vehicles.OfType(Of IRentableVehicle).OrderBy( _
Function(cv) cv.RentalRate)
' Get all non-rentable vehicles
Dim unrentableVehicles = From veh In _
db.Vehicles.OfType(Of Vehicle).Where( _
Function(uv) Not (TypeOf(uv) Is IRentableVehicle))
Rubriques avancées
Création de bases de données
Étant donné que les classes d’entités ont des attributs décrivant la structure des tables et colonnes de base de données relationnelles, il est possible d’utiliser ces informations pour créer de nouvelles instances de votre base de données. Vous pouvez appeler la méthode CreateDatabase() sur le DataContext pour que LINQ to SQL construire une nouvelle base de données instance avec une structure définie par vos objets. Il existe de nombreuses raisons pour lesquelles vous souhaitez peut-être effectuer cette opération : vous créez peut-être une application qui s’installe automatiquement sur un système client, ou une application cliente qui a besoin d’une base de données locale pour enregistrer son état hors connexion. Pour ces scénarios, CreateDatabase() est idéal, en particulier si un fournisseur de données connu comme SQL Server Express 2005 est disponible.
Toutefois, les attributs de données peuvent ne pas tout encoder sur une structure de base de données existante. Le contenu des fonctions, des procédures stockées, des déclencheurs et des contraintes case activée définis par l’utilisateur n’est pas représenté par les attributs. La fonction CreateDatabase() crée uniquement un réplica de la base de données à l’aide des informations qu’elle connaît, à savoir la structure de la base de données et les types de colonnes de chaque table. Pourtant, pour une variété de bases de données, cela suffit.
Voici un exemple de création d’une base de données nommée MyDVDs.mdf :
C#
[Table(Name="DVDTable")]
public class DVD
{
[Column(Id = true)]
public string Title;
[Column]
public string Rating;
}
public class MyDVDs : DataContext
{
public Table<DVD> DVDs;
public MyDVDs(string connection) : base(connection) {}
}
Visual Basic
<Table(Name:="DVDTable")> _
Public Class DVD
<Column(Id:=True)> _
public Title As String
<Column> _
Public Rating As String
End Class
Public Class MyDVDs
Inherits DataContext
Public DVDs As Table(Of DVD)
Public Sub New(connection As String)
End Class
Le modèle objet peut être utilisé pour créer une base de données à l’aide de SQL Server Express 2005 comme suit :
C#
MyDVDs db = new MyDVDs("c:\\mydvds.mdf");
db.CreateDatabase();
Visual Basic
Dim db As MyDVDs = new MyDVDs("c:\mydvds.mdf")
db.CreateDatabase()
LINQ to SQL fournit également une API pour supprimer une base de données existante avant d’en créer une nouvelle. Le code de création de base de données ci-dessus peut être modifié pour d’abord case activée pour une version existante de la base de données à l’aide de DatabaseExists(), puis le supprimer à l’aide de DeleteDatabase().
C#
MyDVDs db = new MyDVDs("c:\\mydvds.mdf");
if (db.DatabaseExists()) {
Console.WriteLine("Deleting old database...");
db.DeleteDatabase();
}
db.CreateDatabase();
Visual Basic
Dim db As MyDVDs = New MyDVDs("c:\mydvds.mdf")
If (db.DatabaseExists()) Then
Console.WriteLine("Deleting old database...")
db.DeleteDatabase()
End If
db.CreateDatabase()
Après l’appel à CreateDatabase(), la nouvelle base de données peut accepter des requêtes et des commandes telles que SubmitChanges() pour ajouter des objets au fichier MDF.
Il est également possible d’utiliser CreateDatabase() avec une référence SKU autre que SQL Server Express, en utilisant un fichier MDF ou simplement un nom de catalogue. Tout dépend de ce que vous utilisez pour votre chaîne de connexion. Les informations contenues dans la chaîne de connexion sont utilisées pour définir la base de données qui existera, pas nécessairement celle qui existe déjà. LINQ to SQL allez extraire les informations pertinentes et les utiliser pour déterminer la base de données à créer et sur quel serveur la créer. Bien sûr, vous aurez besoin de droits d’administrateur de base de données ou équivalents sur le serveur pour ce faire.
Interopérabilité avec ADO.NET
LINQ to SQL fait partie de la famille de technologies ADO.NET. Étant basé sur les services fournis par le modèle de fournisseur ADO.NET, il est possible de combiner du code LINQ to SQL avec des applications ADO.NET existantes.
Lorsque vous créez un LINQ to SQL DataContext, vous pouvez lui fournir une connexion ADO.NET existante. Toutes les opérations sur le DataContext, y compris les requêtes, utilisent la connexion que vous avez fournie. Si la connexion a déjà été ouverte LINQ to SQL respectera votre autorité sur la connexion et la laissera telle quelle lorsque vous aurez terminé. Normalement, LINQ to SQL ferme sa connexion dès qu’une opération est terminée, sauf si une transaction est dans l’étendue.
C#
SqlConnection con = new SqlConnection( ... );
con.Open();
...
// DataContext takes a connection
Northwind db = new Northwind(con);
...
var q =
from c in db.Customers
where c.City == "London"
select c;
Visual Basic
Dim con As SqlConnection = New SqlConnection( ... )
con.Open()
...
' DataContext takes a connection
Dim db As Northwind = new Northwind(con)
...
Dim q = From c In db.Customers _
Where c.City = "London" _
Select c
Vous pouvez toujours accéder à la connexion utilisée par votre DataContext via la propriété Connection et la fermer vous-même.
C#
db.Connection.Close();
Visual Basic
db.Connection.Close()
Vous pouvez également fournir le DataContext avec votre propre transaction de base de données, au cas où votre application en a déjà lancé une et que vous souhaitez que dataContext joue avec elle.
C#
IDbTransaction = con.BeginTransaction();
...
db.Transaction = myTransaction;
db.SubmitChanges();
db.Transaction = null;
Visual Basic
Dim db As IDbTransaction = con.BeginTransaction()
...
db.Transaction = myTransaction
db.SubmitChanges()
db.Transaction = Nothing
Chaque fois qu’une transaction est définie, le DataContext l’utilise chaque fois qu’il émet une requête ou exécute une commande. N’oubliez pas de renvoyer la propriété à null lorsque vous avez terminé.
Toutefois, la méthode recommandée pour effectuer des transactions avec le .NET Framework consiste à utiliser l’objet TransactionScope . Il vous permet d’effectuer des transactions distribuées qui fonctionnent entre des bases de données et d’autres gestionnaires de ressources résidant en mémoire. L’idée est que les étendues de transaction commencent à être bon marché, et qu’elles ne font qu’une promotion complète sur les transactions distribuées lorsqu’elles font effectivement référence à plusieurs bases de données ou plusieurs connexions dans l’étendue de la transaction.
C#
using(TransactionScope ts = new TransactionScope()) {
db.SubmitChanges();
ts.Complete();
}
Visual Basic
Using ts As TransactionScope= New TransactionScope()
db.SubmitChanges()
ts.Complete()
End Using
Exécution directe d’instructions SQL
Les connexions et les transactions ne sont pas la seule façon d’interagir avec ADO.NET. Vous constaterez peut-être que dans certains cas, la fonctionnalité de requête ou d’envoi de modifications du DataContext est insuffisante pour la tâche spécialisée que vous souhaiterez peut-être effectuer. Dans ces circonstances, il est possible d’utiliser dataContext pour émettre des commandes SQL brutes directement à la base de données.
La méthode ExecuteQuery() vous permet d’exécuter une requête SQL brute et convertit le résultat de votre requête directement en objets. Par exemple, en supposant que les données de la classe Customer sont réparties sur deux tables customer1 et customer2, la requête suivante retourne une séquence d’objets Customer .
C#
IEnumerable<Customer> results = db.ExecuteQuery<Customer>(
@"select c1.custid as CustomerID, c2.custName as ContactName
from customer1 as c1, customer2 as c2
where c1.custid = c2.custid"
);
Visual Basic
Dim results As IEnumerable(Of Customer) = _
db.ExecuteQuery(Of Customer)( _
"select c1.custid as CustomerID, " & _
"c2.custName as ContactName " & _
"from customer1 as c1, customer2 as c2 "& _
"where c1.custid = c2.custid" )
Tant que les noms de colonnes dans les résultats tabulaires correspondent aux propriétés de colonne de votre classe d’entité LINQ to SQL matérialisez vos objets à partir de toute requête SQL.
La méthode ExecuteQuery() autorise également les paramètres. Dans le code suivant, une requête paramétrable est exécutée :
C#
IEnumerable<Customer> results = db.ExecuteQuery<Customer>(
"select contactname from customers where city = {0}",
"London"
);
Visual Basic
Dim results As IEnumerable(Of Customer) = _
db.ExecuteQuery(Of Customer)( _
"select contactname from customers where city = {0}", _
"London" )
Les paramètres sont exprimés dans le texte de requête à l’aide de la même notation bouclée utilisée par Console.WriteLine() et String.Format(). En fait, String.Format() est en fait appelé sur la chaîne de requête que vous fournissez, en remplaçant les paramètres avec accolades par des noms de paramètres générés comme p0, @p1 ..., p(n).
Modifier la résolution des conflits
Description
Un conflit de modification se produit lorsque le client tente de soumettre des modifications à un objet et qu’une ou plusieurs valeurs utilisées dans la mise à jour case activée ont été mises à jour dans la base de données depuis la dernière lecture par le client.
Note Seuls les membres mappés en tant que UpdateCheck.Always ou UpdateCheck.WhenChanged participent à des vérifications d’accès concurrentiel optimistes. Aucune case activée n’est effectuée pour les membres marqués UpdateCheck.Never.
La résolution de ce conflit comprend la découverte des membres de l’objet qui sont en conflit, puis le choix de ce qu’il faut faire à ce sujet. Notez que l’accès concurrentiel optimiste peut ne pas être la meilleure stratégie dans votre situation particulière. Parfois, il est parfaitement raisonnable de « laisser la dernière mise à jour gagner ».
Détection, création de rapports et résolution des conflits dans LINQ to SQL
La résolution des conflits est le processus d’actualisation d’un élément en conflit en interrogeant à nouveau la base de données et en conciliant les différences. Lorsqu’un objet est actualisé, le suivi des modifications a les anciennes valeurs d’origine et les nouvelles valeurs de base de données. LINQ to SQL détermine ensuite si l’objet est en conflit ou non. Si c’est le cas, LINQ to SQL détermine les membres impliqués. Si la nouvelle valeur de base de données d’un membre est différente de l’ancienne valeur d’origine (qui a été utilisée pour la mise à jour case activée qui a échoué), il s’agit d’un conflit. Tous les conflits de membres sont ajoutés à une liste de conflits.
Par exemple, dans le scénario suivant, User1 commence à préparer une mise à jour en interrogeant la base de données pour obtenir une ligne. Avant que User1 puisse envoyer les modifications, User2 a modifié la base de données. La soumission de User1 échoue, car les valeurs attendues pour col B et col C ont changé.
Conflit de mise à jour de base de données
Utilisateur | Col A | Col B | Col C |
---|---|---|---|
État d'origine | Alfreds | Maria | Sales |
Utilisateur 1 | Alfred | Marketing | |
Utilisateur 2 | Mary | Service |
Dans LINQ to SQL, les objets qui ne parviennent pas à se mettre à jour en raison de conflits d’accès concurrentiel optimiste entraînent la levée d’une exception (ChangeConflictException). Vous pouvez spécifier si l’exception doit être levée au premier échec ou si toutes les mises à jour doivent être tentées avec les échecs accumulés et signalés dans l’exception.
// [C#]
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
db.SubmitChanges(ConflictMode.ContinueOnConflict);
' [Visual Basic]
db.SubmitChanges(ConflictMode.FailOnFirstConflict)
db.SubmitChanges(ConflictMode.ContinueOnConflict)
Lorsqu’elle est levée, l’exception fournit l’accès à une collection ObjectChangeConflict . Les détails sont disponibles pour chaque conflit (mappé à une seule tentative de mise à jour ayant échoué), y compris l’accès à la liste MemberConflicts . Chaque conflit entre membres mappe à un seul membre dans la mise à jour au cours de laquelle le contrôle d'accès concurrentiel a échoué.
Gestion des conflits
Dans le scénario précédent, User1 a les options RefreshMode décrites ci-dessous pour rapprocher les différences avant de tenter de renvoyer. Dans tous les cas, l’enregistrement sur le client est d’abord « actualisé » en extrayant les données mises à jour de la base de données. Cette action garantit que la prochaine tentative de mise à jour n’échouera pas sur les mêmes vérifications d’accès concurrentiel.
Ici, User1 choisit de fusionner les valeurs de base de données avec les valeurs clientes actuelles afin que les valeurs de base de données soient remplacées uniquement lorsque l’ensemble de modifications actuel a également modifié cette valeur. (Voir l’exemple 1 plus loin dans cette section.)
Dans le scénario ci-dessus, après la résolution des conflits, le résultat dans la base de données est le suivant :
KeepChanges
Col A | Col B | Col C | |
---|---|---|---|
KeepChanges | Alfred (Utilisateur 1) | Mary (Utilisateur 2) | Marketing (utilisateur 1) |
- Col A : La modification de User1 (Alfred) s’affiche.
- Col B : La modification de User2 (Mary) s’affiche. Cette valeur a été fusionnée, car User1 ne l’a pas modifiée.
- Col C : La modification de User1 (Marketing) s’affiche. La modification de l’utilisateur2 (service) n’est pas fusionnée, car l’utilisateur1 a également modifié cet élément.
Ci-dessous, User1 choisit de remplacer toutes les valeurs de base de données par les valeurs actuelles. (Voir l’exemple 2 plus loin dans cette section.)
Après l’actualisation, les modifications de User1 sont envoyées. Le résultat dans la base de données est le suivant :
KeepCurrentValues
Col A | Col B | Col C | |
---|---|---|---|
KeepCurrentValues | Alfred (Utilisateur 1) | Maria (original) | Marketing (utilisateur 1) |
- Col A : La modification de User1 (Alfred) s’affiche.
- Col B : Les restes originaux de Maria ; La modification de User2 est ignorée.
- Col C : La modification de User1 (Marketing) s’affiche. La modification de l’utilisateur2 (service) est ignorée.
Dans le scénario suivant, User1 choisit d’autoriser les valeurs de base de données à remplacer les valeurs actuelles dans le client. (Voir l’exemple 3 plus loin dans cette section.)
Dans le scénario ci-dessus, après la résolution des conflits, le résultat de la base de données est le suivant :
OverwriteCurrentValues
Col A | Col B | Col C | |
---|---|---|---|
OverwriteCurrentValues | Alfreds (original) | Mary (Utilisateur 2) | Service (Utilisateur 2) |
- Col A : La valeur d’origine (Alfreds) demeure ; La valeur de User1 (Alfred) est ignorée.
- Col B : La modification de User2 (Mary) s’affiche.
- Col C : La modification de User2 (Service) s’affiche. La modification de User1 (Marketing) est ignorée.
Une fois les conflits résolus, vous pouvez tenter une nouvelle soumission. Étant donné que cette deuxième mise à jour peut également échouer, envisagez d’utiliser une boucle pour les tentatives de mise à jour.
Exemples
Les extraits de code suivants montrent différents membres et techniques d’information à votre disposition pour la découverte et la résolution des conflits de membres.
Exemple 1
Dans cet exemple, les conflits sont résolus « automatiquement ». Autrement dit, les valeurs de base de données sont fusionnées avec les valeurs clientes actuelles, sauf si le client a également modifié cette valeur (KeepChanges). Aucune inspection ou gestion personnalisée des conflits de membres individuels n’a lieu.
C#
try {
context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
//automerge database values into current for members
//that client has not modified
context.ChangeConflicts.Resolve(RefreshMode.KeepChanges);
}
//submit succeeds on second try
context.SubmitChanges(ConflictMode.FailOnFirstConflict);
Visual Basic
Try
context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
' automerge database values into current for members
' that client has not modified context.ChangeConflicts.Resolve(RefreshMode.KeepChanges)
End Try
' submit succeeds on second try
context.SubmitChanges(ConflictMode.FailOnFirstConflict)
Exemple 2
Dans cet exemple, les conflits sont à nouveau résolus sans aucune gestion personnalisée. Mais cette fois, les valeurs de base de données ne sont pas fusionnées dans les valeurs clientes actuelles.
C#
try {
context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
foreach (ObjectChangeConflict cc in context.ChangeConflicts) {
//No database values are automerged into current
cc.Resolve(RefreshMode.KeepCurrentValues);
}
}
Visual Basic
Try
context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
For Each cc As ObjectChangeConflict In context.ChangeConflicts
‘No database values are automerged into current
cc.Resolve(RefreshMode.KeepCurrentValues)
Next
End Try
Exemple 3
Là encore, aucune gestion personnalisée n’a lieu. Mais dans ce cas, toutes les valeurs clientes sont mises à jour avec les valeurs de base de données actuelles.
C#
try {
context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
foreach (ObjectChangeConflict cc in context.ChangeConflicts) {
//No database values are automerged into current
cc.Resolve(RefreshMode.OverwriteCurrentValues);
}
}
Visual Basic
Try
context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
For Each cc As ObjectChangeConflict In context.ChangeConflicts
' No database values are automerged into current
cc.Resolve(RefreshMode. OverwriteCurrentValues)
Next
End Try
Exemple 4
Cet exemple montre un moyen d’accéder aux informations sur une entité en conflit.
C#
try {
user1.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
Console.WriteLine("Optimistic concurrency error");
Console.ReadLine();
foreach (ObjectChangeConflict cc in user1.ChangeConflicts) {
ITable table = cc.Table;
Customers entityInConflict = (Customers)cc.Object;
Console.WriteLine("Table name: {0}", table.Name);
Console.Write("Customer ID: ");
Console.WriteLine(entityInConflict.CustomerID);
}
}
Visual Basic
Try
context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
Console.WriteLine("Optimistic concurrency error")
Console.ReadLine()
For Each cc As ObjectChangeConflict In context.ChangeConflicts
Dim table As ITable = cc.Table
Dim entityInConflict As Customers = CType(cc.Object, Customers)
Console.WriteLine("Table name: {0}", table.Name)
Console.Write("Customer ID: ")
Console.WriteLine(entityInConflict.CustomerID)
Next
End Try
Exemple 5
Cet exemple montre comment ajouter une boucle à travers les membres individuels. Ici, vous pouvez fournir une gestion personnalisée de n’importe quel membre.
Note Ajouter à l’aide de System.Reflection ; pour fournir MemberInfo.
C#
try {
user1.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
Console.WriteLine("Optimistic concurrency error");
Console.ReadLine();
foreach (ObjectChangeConflict cc in user1.ChangeConflicts) {
ITable table = cc.Table;
Customers entityInConflict = (Customers)cc.Object;
Console.WriteLine("Table name: {0}", table.Name);
Console.Write("Customer ID: ");
Console.WriteLine(entityInConflict.CustomerID);
foreach (MemberChangeConflict mc in cc.MemberConflicts) {
object currVal = mc.CurrentValue;
object origVal = mc.OriginalValue;
object databaseVal = mc.DatabaseValue;
MemberInfo mi = mc. Member;
Console.WriteLine("Member: {0}", mi.Name);
Console.WriteLine("current value: {0}", currVal);
Console.WriteLine("original value: {0}", origVal);
Console.WriteLine("database value: {0}", databaseVal);
Console.ReadLine();
}
}
}
Visual Basic
Try
user1.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
Console.WriteLine("Optimistic concurrency error")
Console.ReadLine()
For Each cc As ObjectChangeConflict In context.ChangeConflicts
Dim table As ITable = cc.Table
Dim entityInConflict As Customers = CType(cc.Object, Customers)
Console.WriteLine("Table name: {0}", table.Name)
Console.Write("Customer ID: ")
Console.WriteLine(entityInConflict.CustomerID)
For Each mc As MemberChangeConflict In cc.MemberConflicts
Dim currVal As Object = mc.CurrentValue
Dim origVal As Object = mc.OriginalValue
Dim databaseVal As Object = mc.DatabaseValue
Dim mi As MemberInfo = mc.Member
Console.WriteLine("Member: {0}", mi.Name)
Console.WriteLine("current value: {0}", currVal)
Console.WriteLine("original value: {0}", origVal)
Console.WriteLine("database value: {0}", databaseVal)
Console.ReadLine()
Next
Next
End Try
Appel de procédures stockées
LiNQ to SQL prend en charge des procédures stockées et des fonctions définies par l’utilisateur. LINQ to SQL mappe ces abstractions définies par la base de données aux objets clients générés par le code, afin que vous puissiez y accéder de manière fortement typée à partir du code client. Vous pouvez facilement découvrir ces méthodes à l’aide d’IntelliSense, et les signatures de méthode ressemblent aussi étroitement que possible aux signatures des procédures et fonctions définies dans la base de données. Un jeu de résultats retourné par un appel à une procédure mappée est une collection fortement typée. LINQ to SQL peut générer automatiquement les méthodes mappées, mais prend également en charge le mappage manuel dans les situations où vous choisissez de ne pas utiliser la génération de code.
LINQ to SQL mappe les procédures stockées et les fonctions aux méthodes via l’utilisation d’attributs. Les attributs StoredProcedure, Parameter et Function prennent tous en charge une propriété Name , et l’attribut Parameter prend également en charge une propriété DBType . Voici deux exemples :
C#
[StoredProcedure()]
public IEnumerable<CustOrderHistResult> CustOrderHist(
[Parameter(Name="CustomerID", DBType="NChar(5)")] string customerID) {
IExecuteResult result = this.ExecuteMethodCall(this,
((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID);
return ((IEnumerable<CustOrderHistResult>)(result.ReturnValue));
}
[Function(Name="[dbo].[ConvertTemp]")]
public string ConvertTemp(string string) { ... }
Visual Basic
<StoredProcedure()> _
Public Function CustOrderHist( _
<Parameter(Name:="CustomerID", DBType:="NChar(5)")> _
customerID As String) As IEnumerable(Of CustOrderHistResult)
Dim result As IExecuteResult = ExecuteMethodCall(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), customerID)
Return CType(result.ReturnValue, IEnumerable(Of CustOrderHistResult))
End Function
<Function(Name:="[dbo].[ConvertTemp]")> _
Public Function ConvertTemp(str As String) As String
Les exemples suivants illustrent les mappages pour différents types de procédures stockées.
Exemple 1
La procédure stockée suivante prend un paramètre d’entrée unique et retourne un entier :
CREATE PROCEDURE GetCustomerOrderCount(@CustomerID nchar(5))
AS
Declare @count int
SELECT @count = COUNT(*) FROM ORDERS WHERE CustomerID = @CustomerID
RETURN @count
La méthode mappée se présente comme suit :
C#
[StoredProcedure(Name = "GetCustomerOrderCount")]
public int GetCustomerOrderCount(
[Parameter(Name = "CustomerID")] string customerID) {
IExecuteResult result = this.ExecuteMethodCall(this,
((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID);
return (int) result.ReturnValue;
}
Visual Basic
<StoredProcedure (Name:="GetCustomerOrderCount")> _
public Function GetCustomerOrderCount( _
<Parameter(Name:= "CustomerID")> customerID As String) As Integer
Dim result As IExecuteResult = ExecuteMethodCall(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), customerID)
return CInt(result.ReturnValue)
End Function
Exemple 2
Lorsqu'une procédure stockée peut retourner plusieurs formes de résultats, le type de retour ne peut pas être fortement typé en une forme de projection unique. Dans l’exemple suivant, la forme de résultat dépend de l’entrée :
CREATE PROCEDURE VariableResultShapes(@shape int)
AS
if(@shape = 1)
select CustomerID, ContactTitle, CompanyName from customers
else if(@shape = 2)
select OrderID, ShipName from orders
La méthode mappée est la suivante :
C#
[StoredProcedure(Name = "VariableResultShapes")]
[ResultType(typeof(Customer))]
[ResultType(typeof(Order))]
public IMultipleResults VariableResultShapes(System.Nullable<int> shape) {
IExecuteResult result = this.ExecuteMethodCallWithMultipleResults(this,
((MethodInfo)(MethodInfo.GetCurrentMethod())), shape);
return (IMultipleResults) result.ReturnValue;
}
Visual Basic
<StoredProcedure(Name:= "VariableResultShapes")> _
<ResultType(typeof(Customer))> _
<ResultType(typeof(Order))> _
public VariableResultShapes(shape As Integer?) As IMultipleResults
Dim result As IExecuteResult =
ExecuteMethodCallWithMultipleResults(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), shape)
return CType(result.ReturnValue, IMultipleResults)
End Function
Vous pouvez utiliser cette procédure stockée comme suit :
C#
IMultipleResults result = db.VariableResultShapes(1);
foreach (Customer c in result.GetResult<Customer>()) {
Console.WriteLine(c.CompanyName);
}
result = db.VariableResultShapes(2);
foreach (Order o in result.GetResult<Order>()) {
Console.WriteLine(o.OrderID);
}
Visual Basic
Dim result As IMultipleResults = db.VariableResultShapes(1)
For Each c As Customer In result.GetResult(Of Customer)()
Console.WriteLine(c.CompanyName)
Next
result = db.VariableResultShapes(2);
For Each o As Order In result.GetResult(Of Order)()
Console.WriteLine(o.OrderID)
Next
}
Ici, vous devez utiliser le modèle GetResult pour obtenir un énumérateur du type correct, en fonction de vos connaissances de la procédure stockée. LINQ to SQL peut générer tous les types de projection possibles, mais n’a aucun moyen de savoir dans quel ordre ils seront retournés. La seule façon de savoir quels types de projection générés correspondent à une méthode mappée consiste à utiliser des commentaires de code générés sur les méthodes.
Exemple 3
Voici le T-SQL d’une procédure stockée qui retourne séquentiellement plusieurs formes de résultats :
CREATE PROCEDURE MultipleResultTypesSequentially
AS
select * from products
select * from customers
LINQ to SQL mapper cette procédure comme dans l’exemple 2 ci-dessus. Dans ce cas, toutefois, il existe deux jeux de résultats séquentiels .
C#
[StoredProcedure(Name="MultipleResultTypesSequentially")]
[ResultType(typeof(Product))]
[ResultType(typeof(Customer))]
public IMultipleResults MultipleResultTypesSequentially() {
return ((IMultipleResults)(
this.ExecuteMethodCallWithMultipleResults (this,
((MethodInfo)(MethodInfo.GetCurrentMethod()))).ReturnValue
)
);
}
Visual Basic
<StoredProcedure(Name:="MultipleResultTypesSequentially")> _
<ResultType(typeof(Customer))> _
<ResultType(typeof(Order))> _
public Function MultipleResultTypesSequentially() As IMultipleResults
Return CType( ExecuteMethodCallWithMultipleResults (Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo)), _
IMultipleResults).ReturnValue
End Function
Vous pouvez utiliser cette procédure stockée comme suit :
C#
IMultipleResults sprocResults = db.MultipleResultTypesSequentially();
//first read products
foreach (Product p in sprocResults.GetResult<Product>()) {
Console.WriteLine(p.ProductID);
}
//next read customers
foreach (Customer c in sprocResults.GetResult<Customer>()){
Console.WriteLine(c.CustomerID);
}
Visual Basic
Dim sprocResults As IMultipleResults = db.MultipleResultTypesSequentially()
' first read products
For Each P As Product In sprocResults.GetResult(Of Product)()
Console.WriteLine(p.ProductID)
Next
' next read customers
For Each c As Customer c In sprocResults.GetResult(Of Customer)()
Console.WriteLine(c.CustomerID)
Next
Exemple 4
LINQ to SQL mappe les out
paramètres aux paramètres de référence (ref mot clé) et, pour les types valeur, déclare le paramètre comme nullable (par exemple, int?). La procédure de l’exemple suivant prend un seul paramètre d’entrée et retourne un out
paramètre.
CREATE PROCEDURE GetCustomerCompanyName(
@customerID nchar(5),
@companyName nvarchar(40) output
)
AS
SELECT @companyName = CompanyName FROM Customers
WHERE CustomerID=@CustomerID
La méthode mappée est la suivante :
C#
[StoredProcedure(Name = "GetCustomerCompanyName")]
public int GetCustomerCompanyName(
string customerID, ref string companyName) {
IExecuteResult result =
this.ExecuteMethodCall(this,
((MethodInfo)(MethodInfo.GetCurrentMethod())),
customerID, companyName);
companyName = (string)result.GetParameterValue(1);
return (int)result.ReturnValue;
}
Visual Basic
<StoredProcedure(Name:="GetCustomerCompanyName")> _
Public Function GetCustomerCompanyName( _
customerID As String, ByRef companyName As String) As Integer
Dim result As IExecuteResult = ExecuteMethodCall(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), customerID, _
companyName)
companyName = CStr(result.GetParameterValue(1))
return CInt(result.ReturnValue)
End Function
Dans ce cas, la méthode n’a pas de valeur de retour explicite, mais la valeur de retour par défaut est quand même mappée. Pour le paramètre de sortie, un paramètre de sortie correspondant est utilisé comme prévu.
Vous devez appeler la procédure stockée ci-dessus comme suit :
C#
string CompanyName = "";
string customerID = "ALFKI";
db.GetCustomerCompanyName(customerID, ref CompanyName);
Console.WriteLine(CompanyName);
Visual Basic
Dim CompanyName As String = ""
Dim customerID As String = "ALFKI"
db.GetCustomerCompanyName(customerID, CompanyName)
Console.WriteLine(CompanyName)
Fonctions définies par l’utilisateur
LINQ to SQL prend en charge les fonctions scalaires et table, et prend en charge l’équivalent en ligne des deux.
LINQ to SQL gère les appels scalaires inline de la même façon que les fonctions définies par le système sont appelées. Considérez la requête suivante :
C#
var q =
from p in db.Products
select
new {
pid = p.ProductID,
unitp = Math.Floor(p.UnitPrice.Value)
};
Visual Basic
Dim productInfos = From prod In db.Products _
Select p.ProductID, price = Math.Floor(p.UnitPrice.Value)
Ici, l’appel de méthode Math.Floor est traduit en appel à la fonction système « FLOOR ». De la même façon, un appel à une fonction mappée à une fonction UDF est traduit en un appel à la fonction UDF dans SQL.
Exemple 1
Voici une fonction scalaire définie par l’utilisateur (UDF) ReverseCustName(). Dans SQL Server, la fonction peut être définie comme suit :
CREATE FUNCTION ReverseCustName(@string varchar(100))
RETURNS varchar(100)
AS
BEGIN
DECLARE @custName varchar(100)
-- Impl. left as exercise for the reader
RETURN @custName
END
Vous pouvez mapper une méthode cliente définie sur une classe de schéma à cette fonction UDF à l’aide du code ci-dessous. Notez que le corps de la méthode construit une expression qui capture l’intention de l’appel de méthode et transmet cette expression au DataContext pour la traduction et l’exécution. (Cette exécution directe se produit uniquement si la fonction est appelée.)
C#
[Function(Name = "[dbo].[ReverseCustName]")]
public string ReverseCustName(string string1) {
IExecuteResult result = this.ExecuteMethodCall(this,
(MethodInfo)(MethodInfo.GetCurrentMethod())), string1);
return (string) result.ReturnValue;
}
Visual Basic
Function(Name:= "[dbo].[ReverseCustName]")> _
Public Function ReverseCustName(string1 As String) As String
Dim result As IExecuteResult = ExecuteMethodCall(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), string1)
return CStr(result.ReturnValue)
Exemple 2
Dans la requête suivante, vous pouvez voir un appel inline à la méthode UDF générée ReverseCustName. Dans ce cas, la fonction n’est pas exécutée immédiatement. Le code SQL créé pour cette requête se traduit par un appel à la fonction UDF définie dans la base de données (consultez le code SQL suivant la requête).
C#
var q =
from c in db.Customers
select
new {
c.ContactName,
Title = db.ReverseCustName(c.ContactTitle)
};
Visual Basic
Dim customerInfos = From cust In db.Customers _
Select c.ContactName, _
Title = db.ReverseCustName(c.ContactTitle)
SELECT [t0].[ContactName],
dbo.ReverseCustName([t0].[ContactTitle]) AS [Title]
FROM [Customers] AS [t0]
Lorsque vous appelez la même fonction en dehors d’une requête, LINQ to SQL crée une requête simple à partir de l’expression d’appel de méthode avec la syntaxe SQL suivante (où le paramètre @p0
est lié à la constante passée) :
Dans LINQ to SQL :
C#
string str = db.ReverseCustName("LINQ to SQL");
Visual Basic
Dim str As String = db.ReverseCustName("LINQ to SQL")
Convertit en :
SELECT dbo.ReverseCustName(@p0)
Exemple 3
Une fonction table (TVF) retourne un seul jeu de résultats (contrairement aux procédures stockées, qui peuvent retourner plusieurs formes de résultats). Étant donné que le type de retour TVF est table, vous pouvez utiliser un OBJET TVF n’importe où dans SQL, et vous pouvez traiter le TVF de la même façon qu’une table.
Considérez la définition de SQL Server suivante d’une fonction table :
CREATE FUNCTION ProductsCostingMoreThan(@cost money)
RETURNS TABLE
AS
RETURN
SELECT ProductID, UnitPrice
FROM Products
WHERE UnitPrice > @cost
Cette fonction indique explicitement qu’elle retourne une table, de sorte que la structure du jeu de résultats retournée est implicitement définie. LINQ to SQL mappe la fonction comme suit :
C#
[Function(Name = "[dbo].[ProductsCostingMoreThan]")]
public IQueryable<Product> ProductsCostingMoreThan(
System.Nullable<decimal> cost) {
return this.CreateMethodCallQuery<Product>(this,
(MethodInfo)MethodInfo.GetCurrentMethod(),
cost);
}
Visual Basic
<Function(Name:="[dbo].[ProductsCostingMoreThan]")> _
Public Function ProductsCostingMoreThan(
cost As System.Nullable(Of Decimal)) As IQueryable(Of Product)
Return CreateMethodCallQuery(Of Product)(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), cost)
Le code SQL suivant montre que vous pouvez joindre la table retournée par la fonction et la traiter comme n’importe quelle autre table :
SELECT p2.ProductName, p1.UnitPrice
FROM dbo.ProductsCostingMoreThan(80.50)
AS p1 INNER JOIN Products AS p2 ON p1.ProductID = p2.ProductID
Dans LINQ to SQL, la requête est rendue comme suit (à l’aide de la nouvelle syntaxe « join ») :
C#
var q =
from p in db.ProductsCostingMoreThan(80.50m)
join s in db.Products on p.ProductID equals s.ProductID
select new {p.ProductID, s.UnitPrice};
Visual Basic
Dim productInfos = From costlyProd In db.ProductsCostingMoreThan(80.50m) _
Join prod In db.Products _
On costlyProd.ProductID Equals prod.ProductID _
Select costlyProd.ProductID, prod.UnitPrice
limitations LINQ to SQL sur les procédures stockées
LINQ to SQL prend en charge la génération de code pour les procédures stockées qui retournent des jeux de résultats déterminés statiquement. Par conséquent, le générateur de code LINQ to SQL ne prend pas en charge les éléments suivants :
- Procédures stockées qui utilisent le sql dynamique pour retourner des jeux de résultats. Lorsqu’une procédure stockée contient une logique conditionnelle pour générer une instruction SQL dynamique, LINQ to SQL ne peut pas acquérir de métadonnées pour le jeu de résultats, car la requête utilisée pour générer le jeu de résultats est inconnue jusqu’à l’exécution.
- Procédures stockées qui génèrent des résultats basés sur une table temporaire.
L’outil Entity Class Generator Tool
Si vous disposez d’une base de données existante, il n’est pas nécessaire de créer manuellement un modèle objet complet pour le représenter. La distribution LINQ to SQL est fournie avec un outil appelé SQLMetal. Il s’agit d’un utilitaire de ligne de commande qui automatise la création de classes d’entité en déduit les classes appropriées à partir des métadonnées de la base de données.
Vous pouvez utiliser SQLMetal pour extraire des métadonnées SQL d’une base de données et générer un fichier source contenant des déclarations de classe d’entité. Vous pouvez également fractionner le processus en deux étapes, d’abord générer un fichier XML représentant les métadonnées SQL, puis traduire ce fichier XML en fichier source contenant des déclarations de classe. Ce processus de fractionnement vous permet de conserver les métadonnées sous forme de fichier afin de pouvoir les modifier. Le processus d’extraction qui produit le fichier fait quelques inférences sur les noms de classes et de propriétés appropriés en fonction des noms de table et de colonne de la base de données. Vous devrez peut-être modifier le fichier XML pour que le générateur produise des résultats plus agréables ou masque des aspects de la base de données que vous ne souhaitez pas présenter dans vos objets.
Le scénario le plus simple pour utiliser SQLMetal consiste à générer directement des classes à partir d’une base de données existante. Voici comment appeler l’outil :
C#
SqlMetal /server:.\SQLExpress /database:Northwind /pluralize /namespace:nwind /code:Northwind.cs
Visual Basic
SqlMetal /server:.\SQLExpress /database:Northwind /pluralize /namespace:nwind /code:Northwind.vb /language:vb
L’exécution de l’outil crée un fichier Northwind.cs ou .vb qui contient le modèle objet généré par la lecture des métadonnées de la base de données. Cette utilisation fonctionne bien si les noms des tables de la base de données sont similaires aux noms des objets que vous souhaitez générer. Si ce n’est pas le cas, vous voudrez adopter l’approche en deux étapes.
Pour indiquer à SQLMetal de générer un fichier DBML, utilisez l’outil comme suit :
SqlMetal /server:.\SQLExpress /database:Northwind /pluralize
/xml:Northwind.dbml
Une fois le fichier dbml généré, vous pouvez l’annoter avec la classe et l’attribut de propriété pour décrire la façon dont les tables et les colonnes sont mappées aux classes et aux propriétés. Une fois que vous avez terminé d’annoter le fichier dbml, vous pouvez générer votre modèle objet en exécutant la commande suivante :
C#
SqlMetal /namespace:nwind /code:Northwind.cs Northwind.dbml
Visual Basic
SqlMetal /namespace:nwind /code:Northwind.vb Northwind.dbml /language:vb
La signature d’utilisation DE SQLMetal est la suivante :
SqlMetal [options] [filename]
Voici un tableau montrant les options de ligne de commande disponibles pour SQLMetal.
Options de ligne de commande pour SQLMetal
Option | Description |
---|---|
/server:<name> | Indique le serveur auquel se connecter pour accéder à la base de données. |
/database:<name> | Indique le nom de la base de données à partir de laquelle lire les métadonnées. |
/user:<name> | Id d’utilisateur de connexion pour le serveur. |
/password:<name> | Mot de passe de connexion pour le serveur. |
/views | Extraire les vues de base de données. |
/functions | Extraire les fonctions de base de données. |
/sprocs | Extrayez les procédures stockées. |
/code[:<filename>] | Indique que la sortie de l’outil est un fichier source de déclarations de classe d’entité. |
/language:<language> | Utilisez Visual Basic ou C# (par défaut). |
/xml[:<filename>] | Indique que la sortie des outils est un fichier DBML décrivant les métadonnées de la base de données et la première approximation des noms de classes et de propriétés. |
/map[:<filename>] | Indique qu’un fichier de mappage externe doit être utilisé au lieu d’attributs. |
/pluralize | Indique que l’outil doit effectuer une heuristique de pluralisation/dé pluralisation de la langue anglaise pour les noms des tables afin de produire les noms de classes et de propriétés appropriés. |
/namespace:<name> | Indique l’espace de noms dans lequel les classes d’entité seront générées. |
/timeout :<secondes> | Valeur de délai d’expiration en secondes à utiliser pour les commandes de base de données. |
Note Pour extraire les métadonnées d’un fichier MDF, vous devez spécifier le nom de fichier MDF après toutes les autres options. Si aucun /server n’est spécifié , localhost est supposé.
Informations de référence dbML de l’outil générateur
Le fichier DBML (Database Mapping Language) est avant tout une description des métadonnées SQL d’une base de données donnée. Elle est extraite par SQLMetal en examinant les métadonnées de la base de données. Le même fichier est également utilisé par SQLMetal pour générer un modèle objet par défaut pour représenter la base de données.
Voici un exemple prototypage de la syntaxe DBML :
<?xml version="1.0" encoding="utf-16"?>
<Database Name="Northwind" EntityNamespace="Mappings.FunctionMapping"
ContextNamespace="Mappings.FunctionMapping"
Provider="System.Data.Linq.SqlClient.Sql2005Provider"
xmlns="https://schemas.microsoft.com/dsltools/LINQ to SQLML">
<Table Name="Categories">
<Type Name="Category">
<Column Name="CategoryID" Type="System.Int32"
DbType="Int NOT NULL IDENTITY" IsReadOnly="False"
IsPrimaryKey="True" IsDbGenerated="True" CanBeNull="False" />
<Column Name="CategoryName" Type="System.String"
DbType="NVarChar(15) NOT NULL" CanBeNull="False" />
<Column Name="Description" Type="System.String"
DbType="NText" CanBeNull="True" UpdateCheck="Never" />
<Column Name="Picture" Type="System.Byte[]"
DbType="Image" CanBeNull="True" UpdateCheck="Never" />
<Association Name="FK_Products_Categories" Member="Products"
ThisKey="CategoryID" OtherKey="CategoryID"
OtherTable="Products" DeleteRule="NO ACTION" />
</Type>
</Table>
<Function Name="GetCustomerOrders">
<Parameter Name="customerID" Type="System.String" DbType="NChar(5)" />
<ElementType Name="GetCustomerOrdersResult">
<Column Name="OrderID" Type="System.Int32"
DbType="Int" CanBeNull="True" />
<Column Name="ShipName" Type="System.String"
DbType="NVarChar(40)" CanBeNull="True" />
<Column Name="OrderDate" Type="System.DateTime"
DbType="DateTime" CanBeNull="True" />
<Column Name="Freight" Type="System.Decimal"
DbType="Money" CanBeNull="True" />
</ElementType>
</Function>
</Database>
Les éléments et leurs attributs sont décrits comme suit.
Base de données
Il s’agit de l’élément le plus externe au format XML. Cet élément est mappé librement à l’attribut Database sur le DataContext généré.
Attributs de base de données
Attribut | Type | Default | Description |
---|---|---|---|
Nom | String | Aucun | Nom de la base de données. S’il est présent et s’il génère un DataContext, lui attache un attribut Database portant ce nom. Également utilisé comme nom de la classe DataContext si l’attribut de classe n’est pas présent. |
EntityNamespace | Fort | Aucun | Espace de noms par défaut pour les classes générées à partir d’éléments Type dans les éléments Table. Si aucun espace de noms n’est spécifié ici, les classes d’entité sont générées dans l’espace de noms racine. |
ContextNamespace | String | Aucun | Espace de noms par défaut pour la classe DataContext générée. Si aucun espace de noms n’est spécifié ici, la classe DataContext est générée dans l’espace de noms racine. |
Classe | String | Database.Name | Nom de la classe DataContext générée. Si aucun élément n’est présent, utilisez l’attribut Name de l’élément Database. |
AccessModifier | AccessModifier | Public | Niveau d’accessibilité de la classe DataContext générée. Les valeurs valides sont Public, Protected, Internal et Private. |
BaseType | String | « System.Data.Linq.DataContext » | Type de base de la classe DataContext . |
Fournisseur | String | « System.Data.Linq.SqlClient.Sql2005Provider » | Fournisseur de DataContext, utilisez le fournisseur Sql2005 comme fournisseur par défaut |
ExternalMapping | Boolean | False | Spécifiez si le DBML est utilisé pour générer un fichier de mappage externe. |
Sérialisation | SérialisationMode | SerializationMode.None | Spécifiez si les classes DataContext et d’entité générées sont sérialisables. |
Attributs de Sub-Element de base de données
Sub-Element | Type d'élément | Plage d’occurrences | Description |
---|---|---|---|
<Table> | Table de charge de travail | 0-sans limite | Représente une table ou une vue SQL Server qui sera mappée à un type unique ou à une hiérarchie d’héritage. |
<Fonction> | Fonction | 0-sans limite | Représente une procédure stockée SQL Server ou une fonction de base de données qui sera mappée à une méthode dans la classe DataContext générée. |
<Connection> | Connexion | 0-1 | Représente la connexion de base de données que ce DataContext utilisera. |
Table de charge de travail
Cet élément représente une table de base de données (ou une vue) qui sera mappée à un type unique ou à une hiérarchie d’héritage. Cet élément est mappé à l’attribut Table sur la classe d’entité générée.
Attributs de table
Attribut | Type | Default | Description |
---|---|---|---|
Nom | String | (obligatoire) | Nom de la table dans la base de données. Sert de base du nom par défaut de l’adaptateur de table, si nécessaire. |
Membre | String | Table.Name | Nom du champ membre généré pour cette table dans la classe DataContext . |
AccessModifier | AccessModifier | Public | Niveau d’accessibilité de la référence table<T> dans le DataContext. Les valeurs valides sont Public, Protected, Internal et Private. |
Attributs de Sub-Element de table
Sub-Element | Type d'élément | Plage d’occurrences | Description |
---|---|---|---|
<Type> | Type | 1-1 | Représente le type ou la hiérarchie d’héritage mappé à cette table. |
<InsertFunction> | TableFunction | 0-1 | Méthode d’insertion. Lorsqu’elle est présente, une méthode InsertT est générée. |
<UpdateFunction> | TableFunction | 0-1 | Méthode de mise à jour. Lorsqu’il est présent, une méthode UpdateT est générée. |
<DeleteFunction> | TableFunction | 0-1 | Méthode de suppression. Lorsqu’elle est présente, une méthode DeleteT est générée. |
Type
Cet élément représente une définition de type pour une forme de résultat table ou une procédure stockée. Cette opération code-gen dans un nouveau type CLR avec les colonnes et les associations spécifiées.
Le type peut également représenter un composant d’une hiérarchie d’héritage, avec plusieurs types mappés à la même table. Dans ce cas, les éléments Type sont imbriqués pour représenter les relations d’héritage parent-enfant et sont différenciés dans la base de données par le code d’héritage spécifié.
Attributs de type
Attribut | Type | Default | Description |
---|---|---|---|
Nom | String | (obligatoire) | Nom du type CLR à générer. |
InheritanceCode | String | None | Si ce type participe à l’héritage, il peut avoir un code d’héritage associé pour faire la distinction entre les types CLR lors du chargement de lignes à partir de la table. Le type dont InheritanceCode correspond à la valeur de la colonne IsDiscriminator est utilisé pour instancier l’objet chargé. Si le code d’héritage n’est pas présent, la classe d’entité générée est abstraite. |
IsInheritanceDefault | Boolean | False | Si cela est vrai pour un type dans une hiérarchie d’héritage, ce type est utilisé lors du chargement de lignes qui ne correspondent à aucun code d’héritage défini. |
AccessModifier | AccessModifier | Public | Niveau d’accessibilité du type CLR en cours de création. Les valeurs valides sont : Public, Protected, Internal et Private. |
Id | String | None | Un type peut avoir un ID unique. L’ID d’un type peut être utilisé par d’autres tables ou fonctions. L’ID apparaît uniquement dans le fichier DBML, pas dans le modèle objet. |
IdRef | String | None | IdRef est utilisé pour faire référence à l’ID d’un autre type. Si IdRef est présent dans un élément de type, l’élément type doit uniquement contenir les informations IdRef . IdRef apparaît uniquement dans le fichier DBML, pas dans le modèle objet. |
Attributs de type Sub-Element
Sub-Element | Type d'élément | Plage d’occurrences | Description |
---|---|---|---|
<Colonne> | Colonne | 0-sans limite | Représente une propriété dans ce type qui sera liée à un champ dans la table de ce type. |
<Association> | Association | 0-sans limite | Représente une propriété dans ce type qui sera liée à une extrémité d’une relation de clé étrangère entre des tables. |
<Type> | Subtype | 0-sans limite | Représente des sous-types de ce type au sein d’une hiérarchie d’héritage. |
Subtype
Cet élément représente un type dérivé dans une hiérarchie d’héritage. Celui-ci est généré dans un nouveau type CLR avec les colonnes et les associations spécifiées dans ce type. Aucun attribut d’héritage n’est généré pour les sous-types.
Par rapport à Type, les éléments SubType n’ont pas AccessModifier , car tous les types dérivés doivent être publics. Les sous-types ne peuvent pas être réutilisés par d’autres tables et fonctions, de sorte qu’il n’y a pas d’Id et d’IdRef .
Attributs SubType
Attribut | Type | Default | Description |
---|---|---|---|
Nom | String | (obligatoire) | Nom du type CLR à générer. |
InheritanceCode | String | Aucun | Si ce type participe à l’héritage, il peut avoir un code d’héritage associé pour faire la distinction entre les types CLR lors du chargement de lignes à partir de la table. Le type dont l’héritageCode correspond à la valeur de la colonne IsDiscriminator est utilisé pour instancier l’objet chargé. Si le code d’héritage n’est pas présent, la classe d’entité générée est abstraite. |
IsInheritanceDefault | Boolean | False | Si cela est vrai pour un type dans une hiérarchie d’héritage, ce type sera utilisé lors du chargement de lignes qui ne correspondent à aucun code d’héritage défini. |
Attributs Sub-Element subType
Sub-Element | Type d'élément | Plage d’occurrences | Description |
---|---|---|---|
<Colonne> | Colonne | 0-sans limite | Représente une propriété au sein de ce type qui sera liée à un champ dans la table de ce type. |
<Association> | Association | 0-sans limite | Représente une propriété au sein de ce type qui sera liée à l’une des extrémités d’une relation de clé étrangère entre les tables. |
<Type> | Subtype | 0-sans limite | Représente les sous-types de ce type au sein d’une hiérarchie d’héritage. |
Colonne
Cet élément représente une colonne d’une table qui est mappée à une propriété (et à un champ de stockage) au sein d’une classe. Toutefois, aucun élément Column n’est présent pour l’une ou l’autre des extrémités d’une relation de clé étrangère, car celui-ci est entièrement représenté (aux deux extrémités) par les éléments Association.
Attributs de colonne
Attributs | Type | Default | Description |
---|---|---|---|
Nom | String | Aucun | Nom du champ de base de données auquel cette colonne sera mappées. |
Membre | String | Nom | Nom de la propriété CLR à générer sur le type contenant. |
Stockage | String | _Membre | Nom du champ de stockage CLR privé qui stockera la valeur de cette colonne. Ne supprimez pas stockage lors de la sérialisation, même s’il est par défaut. |
AccessModifier | AccessModifier | Public | Niveau d’accessibilité de la propriété CLR en cours de création. Les valeurs valides sont les suivantes : Public, Protected, Internal et Private. |
Type | String | (obligatoire) | Nom du type de la propriété CLR et du champ de stockage en cours de création. Il peut s’agir d’un nom complet au nom direct d’une classe, à condition que le nom soit finalement dans l’étendue lorsque le code généré est compilé. |
DbType | String | Aucun | Type SQL Server complet (y compris les annotations telles que NOT NULL) pour cette colonne. Utilisé par LINQ to SQL si vous le fournissez pour optimiser les requêtes générées et pour être plus spécifique lors de l’exécution de CreateDatabase(). Sérialisez toujours DbType. |
IsReadOnly | Boolean | False | Si IsReadOnly est défini, un setter de propriétés n’est pas créé, ce qui signifie que les utilisateurs ne peuvent pas modifier la valeur de cette colonne à l’aide de cet objet. |
IsPrimaryKey | Boolean | False | Indique que cette colonne participe à la clé primaire de la table. Ces informations sont nécessaires pour que LINQ to SQL fonctionne correctement. |
IsDbGenerated | Boolean | False | Indique que les données de ce champ sont générées par la base de données. C’est principalement le cas des champs AutoNumber et des champs calculés. Il n’est pas significatif d’affecter des valeurs à ces champs. Par conséquent, ils sont automatiquement IsReadOnly. |
CanBeNull | Boolean | Aucun | Indique que la valeur peut contenir la valeur null. Si vous souhaitez utiliser des valeurs null dans le CLR, vous devez toujours spécifier le ClrType comme T> nullable<. |
UpdateCheck | UpdateCheck | Always (sauf si au moins un autre membre a IsVersion défini, puis Jamais) | Indique si LINQ to SQL devez utiliser cette colonne lors de la détection de conflits d’accès concurrentiel optimiste. En règle générale, toutes les colonnes participent par défaut, sauf s’il existe une colonne IsVersion , qui participe ensuite par elle-même. Peut être : Always, Never ou WhenChanged (ce qui signifie que la colonne participe si sa propre valeur a changé). |
IsDiscriminator | Boolean | False | Indique si ce champ contient le code discriminateur utilisé pour choisir entre les types dans une hiérarchie d’héritage. |
Expression | String | Aucun | N’affecte pas le fonctionnement de LINQ to SQL, mais est utilisé pendant .CreateDatabase() en tant qu’expression SQL brute représentant l’expression de colonne calculée. |
IsVersion | Boolean | False | Indique que ce champ représente un champ TIMESTAMP dans SQL Server qui est automatiquement mis à jour chaque fois que la ligne est modifiée. Ce champ peut ensuite être utilisé pour permettre une détection plus efficace des conflits d’accès concurrentiel optimiste. |
IsDelayLoaded | Boolean | False | Indique que cette colonne ne doit pas être chargée immédiatement lors de la matérialisation de l’objet, mais uniquement lorsque la propriété appropriée est utilisée pour la première fois. Cela est utile pour les champs de mémo volumineux ou les données binaires dans une ligne qui n’est pas toujours nécessaire. |
AutoSync | AutoSync | If (IsDbGenerated && IsPrimaryKey) OnInsert; Sinon si (IsDbGenerated) Always Sinon jamais |
Spécifie si la colonne est automatiquement synchronisée à partir de la valeur générée par la base de données. Les valeurs valides pour cette balise sont : OnInsert, Always et Never. |
Association
Cet élément représente l’une ou l’autre extrémité d’une relation de clé étrangère. Pour les relations un-à-plusieurs, il s’agit d’un EntitySet<T> d’un côté et d’un EntityRef<T> du côté plusieurs. Pour les relations un-à-un, il s’agit d’un EntityRef<T> des deux côtés.
Notez qu’il n’est pas nécessaire d’avoir une entrée Association des deux côtés d’une association. Dans ce cas, une propriété est générée uniquement sur le côté qui contient l’entrée (formant une relation unidirectionnelle).
Attributs d’association
Attribut | Type | Default | Description |
---|---|---|---|
Nom | String | (obligatoire) | Nom de la relation (généralement le nom de la contrainte de clé étrangère). Cela peut être techniquement facultatif, mais doit toujours être généré par le code pour éviter toute ambiguïté lorsqu’il existe plusieurs relations entre les deux mêmes tables. |
Membre | String | Nom | Nom de la propriété CLR à générer de ce côté de l’association. |
Stockage | String |
Si OneToMany et Not isForeignKey :
_OtherTable Autre: _TypeName(OtherTable) |
Nom du champ de stockage CLR privé qui stockera la valeur de cette colonne. |
AccessModifier | AccessModifier | Public | Niveau d’accessibilité de la propriété CLR en cours de création. Les valeurs valides sont Public, Protected, Internal et Private. |
ThisKey | String | Propriété IsIdentity dans la classe conteneur | Liste séparée par des virgules des clés de ce côté de l’association. |
OtherTable | String | Consultez la description. | Table située à l’autre extrémité de la relation. Normalement, cela peut être déterminé par le runtime LINQ to SQL en faisant correspondre des noms de relation, mais cela n’est pas possible pour les associations unidirectionnelles ou anonymes. |
OtherKey | String | Clés primaires dans la classe étrangère | Liste séparée par des virgules des clés de l’autre côté de l’association. |
IsForeignKey | Boolean | False | Indique s’il s’agit du côté « enfant » de la relation, le côté plusieurs d’un un-à-plusieurs. |
RelationshipType | RelationshipType | OneToMany | Indique si l’utilisateur affirme que les données associées à par cette association répondent aux critères des données un-à-un ou correspondent au cas plus général de un-à-plusieurs. Pour un-à-un, l’utilisateur affirme que pour chaque ligne du côté de la clé primaire (« un »), il n’y a qu’une seule ligne côté clé étrangère (« plusieurs »). Cela entraîne la génération d’un T EntityRef<> côté « un » au lieu d’un EntitySet<T>. Les valeurs valides sont OneToOne et OneToMany. |
DeleteRule | String | Aucun | Permet d’ajouter le comportement de suppression à cette association. Par exemple, « CASCADE » ajoute « ONDELETECASCADE » à la relation FK. Si la valeur est null, aucun comportement de suppression n’est ajouté. |
Fonction
Cet élément représente une procédure stockée ou une fonction de base de données. Pour chaque nœud Function , une méthode est générée dans la classe DataContext .
Attributs de fonctions
Attribut | Type | Default | Description |
---|---|---|---|
Nom | String | (obligatoire) | Nom de la procédure stockée dans la base de données. |
Méthode | String | Méthode | Nom de la méthode CLR à générer qui autorise l’appel de la procédure stockée. Le nom par défaut de La méthode comporte des éléments tels que [dbo]. |
AccessModifier | AccessModifier | Public | Niveau d’accessibilité de la méthode de procédure stockée. Les valeurs valides sont Public, Protected, Internal et Private. |
HasMultipleResults | Boolean | Nombre de types > 1 | Spécifie si la procédure stockée représentée par ce nœud Function retourne plusieurs jeux de résultats. Chaque jeu de résultats est une forme tabulaire, il peut s’agir d’un type existant ou d’un ensemble de colonnes. Dans ce dernier cas, un nœud Type est créé pour le jeu de colonnes. |
IsComposable | Boolean | False | Spécifie si la fonction/procédure stockée peut être composée dans des requêtes LINQ to SQL. Seules les fonctions de base de données qui ne retournent pas void peuvent être composées. |
Attributs de Sub-Element de fonction
Sub-Element | Types d’éléments | Plage d’occurrences | Description |
---|---|---|---|
<Paramètre> | Paramètre | 0-sans limite | Représente les paramètres d’entrée et de sortie de cette procédure stockée. |
<ElementType> | Type | 0-sans limite | Représente les formes tabulaires que la procédure stockée correspondante peut retourner. |
<Return> | Renvoie | 0-1 | Type scalaire retourné de cette fonction de base de données ou procédure stockée. Si Return a la valeur null, la fonction retourne void. Une fonction ne peut pas avoir à la fois Return et ElementType. |
TableFunction
Cet élément représente les fonctions de remplacement CUD pour les tables. Le concepteur LINQ to SQL permet de créer des méthodes de remplacement Insert, Update et Delete pour LINQ TO SQL et autorise le mappage des noms de propriétés d’entité aux noms de paramètres de procédure stockée.
Le nom de la méthode pour les fonctions CUD étant fixe, il n’existe aucun attribut Method dans DBML pour les éléments TableFunction . Par exemple, pour la table Customer, les méthodes CUD sont nommées InsertCustomer, UpdateCustomer et DeleteCustomer.
Une fonction de table ne peut pas retourner une forme tabulaire, donc il n’y a pas d’attribut ElementType dans l’élément TableFunction .
Attributs TableFunction
Attribut | Type | Default | Description |
---|---|---|---|
Nom | String | (obligatoire) | Nom de la procédure stockée dans la base de données. |
AccessModifier | AccessModifier | Privées | Niveau d’accessibilité de la méthode de procédure stockée. Les valeurs valides sont Public, Protected, Internal et Private. |
HasMultipleResults | Boolean | Nombre de types > 1 | Spécifie si la procédure stockée représentée par ce nœud Function retourne plusieurs jeux de résultats. Chaque jeu de résultats est une forme tabulaire, il peut s’agir d’un type existant ou d’un ensemble de colonnes. Dans ce dernier cas, un nœud Type est créé pour le jeu de colonnes. |
IsComposable | Boolean | False | Spécifie si la fonction/procédure stockée peut être composée dans des requêtes LINQ to SQL. Seules les fonctions de base de données qui ne retournent pas void peuvent être composées. |
Attributs Sub-Element TableFunction
Sub-Elements | Type d'élément | Plage d’occurrences | Description |
---|---|---|---|
<Paramètre> | TableFunctionParameter | 0-sans limite | Représente les paramètres d’entrée et de sortie de cette fonction de table. |
<Return> | TableFunctionReturn | 0-1 | Type scalaire retourné de cette fonction de table. Si Return a la valeur null, la fonction retourne void. |
Paramètre
Cet élément représente une procédure stockée/paramètre de fonction. Les paramètres peuvent transmettre des données entrantes et sortantes.
Attributs de paramètre
Attribut | Type | Default | Descriptions |
---|---|---|---|
Nom | String | (obligatoire) | Nom de base de données du paramètre proc/function stocké. |
Paramètre | String | Nom | Nom CLR du paramètre de méthode. |
String | (obligatoire) | Nom CLR du paramètre de méthode. | |
DbType | String | Aucun | Type de base de données du paramètre proc/function stocké. |
Sens | ParameterDirection | Dans | Direction dans laquelle le paramètre circule. Il peut s’agir de l’un des éléments In, Out et InOut. |
Renvoie
Cet élément représente le type de retour d’une procédure/fonction stockée.
Attributs de retour
Attribut | Type | Default | Description |
---|---|---|---|
Type | String | (obligatoire) | Type CLR du résultat de la proc/fonction stockée. |
DbType | String | Aucun | Type de base de données du résultat de la proc/fonction stockée. |
TableFunctionParameter
Cet élément représente un paramètre d’une fonction CUD. Les paramètres peuvent transmettre des données dans et hors. Chaque paramètre est mappé à une colonne Table à laquelle cette fonction CUD appartient. Il n’existe aucun attribut Type ou DbType dans cet élément, car les informations de type peuvent être obtenues à partir de la colonne à laquelle le paramètre est mappé.
Attributs TableFunctionParameter
Attribut | Type | Default | Description |
---|---|---|---|
Nom | String | (obligatoire) | Nom de la base de données du paramètre de fonction CUD. |
Paramètre | String | Nom | Nom CLR du paramètre de méthode. |
Colonne | String | Nom | Nom de la colonne à laquelle ce paramètre est mappé. |
Sens | ParameterDirection | Dans | Direction dans laquelle le paramètre circule. Il peut s’agir de l’un des éléments In, Out ou InOut. |
Version | Version | Actuel | Indique si PropertyName fait référence à la version actuelle ou à la version d’origine d’une colonne donnée. Applicable uniquement pendant le remplacement de la mise à jour . Peut être actuel ou d’origine. |
TableFunctionReturn
Cet élément représente un type de retour d’une fonction CUD. En fait, il contient uniquement le nom de colonne mappé au résultat de la fonction CUD. Les informations de type du retour peuvent être obtenues à partir de la colonne .
Attribut TableFunctionReturn
Attrobite | Type | Default | Description |
---|---|---|---|
Colonne | String | Aucun | Nom de colonne auquel le retour est mappé. |
Connexion
Cet élément représente les paramètres de connexion de base de données par défaut. Cela permet de créer un constructeur par défaut pour le type DataContext qui sait déjà se connecter à une base de données.
Il existe deux types de connexions par défaut possibles, l’un avec une chaîne de connexion directe et l’autre qui lit à partir d’App.Settings.
Attributs de connexion
Attribut | Type | Default | Description |
---|---|---|---|
UtiliserApplicationSettings | Boolean | False | Détermine s’il faut utiliser un fichier App.Settings ou obtenir les paramètresde l’application à partir d’une chaîne de connexion directe. |
ConnectionString | String | Aucun | Chaîne de connexion à envoyer au fournisseur de données SQL. |
SettingsObjectName | String | Paramètres | Objet App.Settings à partir duquel récupérer des propriétés. |
ParamètresPropertyName | String | ConnectionString | Propriété App.Settings qui contient connectionString. |
Entités multiniveau
Dans les applications à deux niveaux, un seul DataContext gère les requêtes et les mises à jour. Toutefois, pour les applications avec des niveaux supplémentaires, il est souvent nécessaire d’utiliser des instances DataContext distinctes pour les requêtes et les mises à jour. Par exemple, en cas de ASP.NET applications, la requête et la mise à jour sont effectuées pour les requêtes distinctes adressées au serveur web. Par conséquent, il n’est pas pratique d’utiliser la même instance DataContext sur plusieurs requêtes. Dans ce cas, une instance DataContext doit être en mesure de mettre à jour des objets qu’elle n’a pas récupérés. La prise en charge des entités multiniveau dans LINQ to SQL fournit une telle fonctionnalité via la méthode Attach().
Voici un exemple de modification d’un objet Customer à l’aide d’un autre instance DataContext :
C#
// Customer entity changed on another tier – for example, through a browser
// Back on the mid-tier, a new context needs to be used
Northwind db2 = new Northwind(…);
// Create a new entity for applying changes
Customer C2 = new Customer();
C2.CustomerID ="NewCustID";
// Set other properties needed for optimistic concurrency check
C2.CompanyName = "New Company Name Co.";
...
// Tell LINQ to SQL to track this object for an update; that is, not for insertion
db2.Customers.Attach(C2);
// Now apply the changes
C2.ContactName = "Mary Anders";
// DataContext now knows how to update the customer
db2.SubmitChanges();
Visual Basic
' Customer entity changed on another tier – for example, through a browser
' Back on the mid-tier, a new context needs to be used
Dim db2 As Northwind = New Northwind(…)
' Create a new entity for applying changes
Dim C2 As New Customer()
C2.CustomerID =”NewCustID”
' Set other properties needed for optimistic concurrency check
C2.CompanyName = ”New Company Name Co.”
...
' Tell LINQ to SQL to track this object for an update; that is, not for insertion
db2.Customers.Attach(C2)
' Now apply the changes
C2.ContactName = "Mary Anders"
' DataContext now knows how to update the customer
db2.SubmitChanges()
Dans les applications multiniveau, l’entité entière n’est souvent pas envoyée entre les niveaux pour des raisons de simplicité, d’interopérabilité ou de confidentialité. Par exemple, un fournisseur peut définir un contrat de données pour un service Web qui diffère de l’entité Order utilisée sur le niveau intermédiaire. De même, une page Web peut afficher uniquement un sous-ensemble des membres d’une entité Employee. Par conséquent, la prise en charge multiniveau est conçue pour prendre en charge de tels cas. Seuls les membres appartenant à une ou plusieurs des catégories suivantes doivent être transportés entre les niveaux et définis avant d’appeler Attach().
- Membres qui font partie de l’identité de l’entité.
- Membres qui ont été modifiés.
- Membres qui participent à des case activée d’accès concurrentiel optimiste.
Si un horodatage ou une colonne de numéro de version est utilisé pour l’accès concurrentiel optimiste case activée, le membre correspondant doit être défini avant d’appeler Attach().. Les valeurs des autres membres ne doivent pas être définies avant d’appeler Attach().. LINQ to SQL utilise des mises à jour minimales avec des contrôles d’accès concurrentiel optimistes ; autrement dit, un membre qui n’est pas défini ou vérifié pour l’accès concurrentiel optimiste est ignoré.
Les valeurs d’origine requises pour les vérifications d’accès concurrentiel optimiste peuvent être conservées à l’aide de divers mécanismes en dehors de l’étendue des API LINQ to SQL. Une application ASP.NET peut utiliser un état d’affichage (ou un contrôle qui utilise l’état d’affichage). Un service Web peut utiliser datacontract pour une méthode de mise à jour afin de s’assurer que les valeurs d’origine sont disponibles pour le traitement des mises à jour. Dans un souci d’interopérabilité et de généralité, LINQ to SQL ne détermine pas la forme des données échangées entre les niveaux ni les mécanismes utilisés pour effectuer un aller-retour sur les valeurs d’origine.
Les entités pour l’insertion et la suppression ne nécessitent pas la méthode Attach(). Les méthodes utilisées pour les applications à deux niveaux , Table.Add()
et Table.Remove() peuvent être utilisées pour l’insertion et la suppression. Comme dans le cas des mises à jour à deux niveaux, un utilisateur est responsable de la gestion des contraintes de clé étrangère. Un client avec des commandes ne peut pas être simplement supprimé sans gérer ses commandes s’il existe une contrainte de clé étrangère dans la base de données qui empêche la suppression d’un client avec des commandes.
LINQ to SQL gère également la pièce jointe des entités pour les mises à jour transitivement. L’utilisateur crée essentiellement le graphique d’objets de pré-mise à jour comme il le souhaite et appelle Attach(). Toutes les modifications peuvent ensuite être « relues » sur le graphique attaché pour effectuer les mises à jour nécessaires, comme indiqué ci-dessous :
C#
Northwind db1 = new Northwind(…);
// Assume Customer c1 and related Orders o1, o2 are retrieved
// Back on the mid-tier, a new context needs to be used
Northwind db2 = new Northwind(…);
// Create new entities for applying changes
Customer c2 = new Customer();
c2.CustomerID = c.CustomerID;
Order o2 = new Order();
o2.OrderID = ...;
c2.Orders.Add(o2);
// Add other related objects needed for updates
// Set properties needed for optimistic concurrency check
...
// Order o1 to be deleted
Order o1 = new Order();
o1.OrderID = ...;
// Tell LINQ to SQL to track the graph transitively
db2.Customers.Attach(c2);
// Now "replay" all the changes
// Updates
c2.ContactName = ...;
o2.ShipAddress = ...;
// New object for insertion
Order o3 = new Order();
o3.OrderID = ...;
c2.Orders.Add(o3);
// Remove order o1
db2.Orders.Remove(o1);
// DataContext now knows how to do update/insert/delete
db2.SubmitChanges();
Visual Basic
Dim db1 As Northwind = New Northwind(…)
' Assume Customer c1 and related Orders o1, o2 are retrieved
' Back on the mid-tier, a new context needs to be used
Dim db2 As Northwind = New Northwind(…)
' Create new entities for applying changes
Customer c2 = new Customer()
c2.CustomerID = c.CustomerID
Dim o2 As Order = New Order()
o2.OrderID = ...
c2.Orders.Add(o2)
' Add other related objects needed for updates
' Set properties needed for optimistic concurrency check
...
' Order o1 to be deleted
Dim o1 As Order = New Order()
o1.OrderID = ...
' Tell LINQ to SQL to track the graph transitively
db2.Customers.Attach(c2)
' Now "replay" all the changes
' Updates
c2.ContactName = ...
o2.ShipAddress = ...
' New object for insertion
Dim o3 As Order = New Order()
o3.OrderID = ...
c2.Orders.Add(o3)
' Remove order o1
db2.Orders.Remove(o1)
' DataContext now knows how to do update/insert/delete
db2.SubmitChanges()
Mappage externe
Outre le mappage basé sur les attributs, LINQ to SQL prend également en charge le mappage externe. La forme la plus courante de mappage externe est un fichier XML. Les fichiers de mappage permettent d’autres scénarios où la séparation du mappage du code est souhaitable.
DataContext fournit un constructeur supplémentaire pour fournir un MappingSource. Une forme de MappingSource est un XmlMappingSource qui peut être construit à partir d’un fichier de mappage XML.
Voici un exemple d’utilisation du fichier de mappage :
C#
String path = @"C:\Mapping\NorthwindMapping.xml";
XmlMappingSource prodMapping =
XmlMappingSource.FromXml(File.ReadAllText(path));
Northwind db = new Northwind(
@"Server=.\SQLExpress;Database=c:\Northwind\Northwnd.mdf",
prodMapping
);
Visual Basic
Dim path As String = "C:\Mapping\NorthwindMapping.xml"
Dim prodMapping As XmlMappingSource = _
XmlMappingSource.FromXml(File.ReadAllText(path))
Dim db As Northwind = New Northwind( _
"Server=.\SQLExpress;Database=c:\Northwind\Northwnd.mdf", _
prodMapping )
Voici un extrait de code correspondant du fichier de mappage montrant le mappage pour la classe Product . Il montre la classe Product dans le mappage d’espace de noms mappé à la table Products de la base de données Northwind . Les éléments et attributs sont cohérents avec les noms d’attributs et les paramètres.
<?xml version="1.0" encoding="utf-8"?>
<Database xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="Northwind"
ProviderType="System.Data.Linq.SqlClient.Sql2005Provider">
<Table Name="Products">
<Type Name="Mappings.FunctionMapping.Product">
<Column Name="ProductID" Member="ProductID" Storage="_ProductID"
DbType="Int NOT NULL IDENTITY" IsPrimaryKey="True"
IsDBGenerated="True" AutoSync="OnInsert" />
<Column Name="ProductName" Member="ProductName" Storage="_ProductName"
DbType="NVarChar(40) NOT NULL" CanBeNull="False" />
<Column Name="SupplierID" Member="SupplierID" Storage="_SupplierID"
DbType="Int" />
<Column Name="CategoryID" Member="CategoryID" Storage="_CategoryID"
DbType="Int" />
<Column Name="QuantityPerUnit" Member="QuantityPerUnit"
Storage="_QuantityPerUnit" DbType="NVarChar(20)" />
<Column Name="UnitPrice" Member="UnitPrice" Storage="_UnitPrice"
DbType="Money" />
<Column Name="UnitsInStock" Member="UnitsInStock" Storage="_UnitsInStock"
DbType="SmallInt" />
<Column Name="UnitsOnOrder" Member="UnitsOnOrder" Storage="_UnitsOnOrder"
DbType="SmallInt" />
<Column Name="ReorderLevel" Member="ReorderLevel" Storage="_ReorderLevel"
DbType="SmallInt" />
<Column Name="Discontinued" Member="Discontinued" Storage="_Discontinued"
DbType="Bit NOT NULL" />
<Association Name="FK_Order_Details_Products" Member="OrderDetails"
Storage="_OrderDetails" ThisKey="ProductID" OtherTable="Order Details"
OtherKey="ProductID" DeleteRule="NO ACTION" />
<Association Name="FK_Products_Categories" Member="Category"
Storage="_Category" ThisKey="CategoryID" OtherTable="Categories"
OtherKey="CategoryID" IsForeignKey="True" />
<Association Name="FK_Products_Suppliers" Member="Supplier"
Storage="_Supplier" ThisKey="SupplierID" OtherTable="Suppliers"
OtherKey="SupplierID" IsForeignKey="True" />
</Type>
</Table>
</Database>
Prise en charge et remarques des fonctions NET Framework
Les paragraphes suivants fournissent des informations de base sur la prise en charge de LINQ to SQL type et les différences par rapport au .NET Framework.
Types primitifs
Implémenté
- Opérateurs arithmétiques et de comparaison
- Opérateurs shift : << et >>
- La conversion entre char et numeric est effectuée par UNICODE
/
NCHAR ; sinon , la conversion de SQL est utilisée.
Non implémenté
- <Tapez>. Parse
- Les énumérations peuvent être utilisées et mappées à des entiers et des chaînes dans une table. Pour ce dernier, les méthodes Parse et ToString() sont utilisées.
Différence par rapport à .NET
- La sortie de ToString pour double utilise CONVERT(NVARCHAR(30), @x, 2) sur SQL, qui utilise toujours 16 chiffres et la « notation scientifique ». Par exemple : « 0.0000000000000000e+000 » pour 0, de sorte qu’il ne donne pas la même chaîne que . Convert.ToString() de NET.
System.String
Implémenté
Méthodes non statiques :
- Length, Substring, Contains, StartsWith, EndsWith, IndexOf, Insert, Remove, Replace, Trim, ToLower, ToUpper, LastIndexOf, PadRight, PadLeft, Equals, CompareTo. Toutes les signatures sont prises en charge, sauf quand elles prennent le paramètre StringComparison , etc., comme indiqué ci-dessous.
Méthodes statiques :
Concat(...) all signatures Compare(String, String) String (indexer) Equals(String, String)
Constructeur:
String(Char, Int32)
Opérateurs:
+, ==, != (+, =, and <> in Visual Basic)
Non implémenté
Méthodes qui prennent ou produisent un tableau de caractères.
Méthodes qui prennent unechaîne CultureInfoComparison//IFormatProvider.
Statique (partagé en Visual Basic) :
Copy(String str) Compare(String, String, Boolean) Compare(String, String, StringComparison) Compare(String, String, Boolean, CultureInfo) Compare(String, Int32, String, Int32, Int32) Compare(String, Int32, String, Int32, Int32, Boolean) Compare(String, Int32, String, Int32, Int32, StringComparison) Compare(String, Int32, String, Int32, Int32, Boolean, CultureInfo) CompareOrdinal(String, String) CompareOrdinal(String, Int32, String, Int32, Int32) Join(String, ArrayOf String [,...]) All Join version with first three args
Instance :
ToUpperInvariant() Format(String, Object) + overloads IndexOf(String, Int32, StringComparison) IndexOfAny(ArrayOf Char) Normalize() Normalize(NormalizationForm) IsNormalized() Split(...) StartsWith(String, StringComparison) ToCharArray() ToUpper(CultureInfo) TrimEnd(ParamArray Char) TrimStart(ParamArray Char)
Restrictions/différence par rapport à .NET
SQL utilise des classements pour déterminer l’égalité et l’ordre des chaînes. Ils peuvent être spécifiés sur une instance SQL Server, une base de données, une colonne de table ou une expression.
Les traductions des fonctions implémentées jusqu’à présent ne modifient pas le classement et ne spécifient pas de classement différent sur les expressions traduites. Par conséquent, si le classement par défaut ne respecte pas la casse, des fonctions telles que CompareTo ou IndexOf peuvent donner des résultats différents de ce que les fonctions .NET (sensibles à la casse) donneraient.
Les méthodes StartsWith(str)/
EndsWith(str) supposent que l’argument str est une constante ou une expression évaluée sur le client. Autrement dit, il n’est actuellement pas possible d’utiliser une colonne pour str.
System.Math
Méthodes statiques implémentées
- Toutes les signatures :
- Abs, Acos, Asin, Atan, Atan2, BigMul, Ceiling, Cos, Cosh, Exp, Floor, Log, Log10, Max, Min, Pow, Sign, Sinh, Sqrt, Tan, Tan, Tanh ou Truncate.
Non implémenté
- IEEERemainder.
- DivRem a un paramètre out. Vous ne pouvez donc pas l’utiliser dans une expression. Les constantes Math.PI et Math.E étant évaluées sur le client, elles n’ont pas besoin d’une traduction.
Différence par rapport à .NET
La traduction de la fonction .NET Math.Round est la fonction SQL ROUND. La traduction est prise en charge uniquement lorsqu’une surcharge est spécifiée qui indique la valeur d’énumération MidpointRounding . MidpointRounding.AwayFromZero est un comportement SQL et MidpointRounding.ToEven indique le comportement CLR.
System.Convert
Implémenté
- Méthodes de la forme To<Type1>(<Type2> x) où Type1, Type2 est l’une des suivantes :
- bool, byte, char, DateTime, decimal, double, float, Int16, Int32, Int64 ou string.
- Le comportement est le même qu’un cast :
- Pour ToString(Double), il existe un code spécial pour obtenir la précision totale.
- Pour la conversion Int32/Char, LINQ to SQL utilise la fonction UNICODE
/
NCHAR de SQL. - Sinon, la traduction est un CONVERT.
Non implémenté
ToSByte, UInt16, 32, 64 : ces types n’existent pas dans SQL.
To<integer type>(String, Int32) ToString(..., Int32) any overload ending with an Int32 toBase IsDBNull(Object) GetTypeCode(Object) ChangeType(...)
Versions avec le paramètre IFormatProvider .
Méthodes qui impliquent un tableau (To/FromBase64CharArray
,
To/FromBase64String).
System.TimeSpan
Implémenté
Constructeurs :
TimeSpan(Long) TimeSpan (year, month, day) TimeSpan (year, month, day, hour, minutes, seconds) TimeSpan (year, month, day, hour, minutes, seconds, milliseconds)
Opérateurs:
Comparison operators: <,==, and so on in C#; <, =, and so on in Visual Basic +, -
Méthodes statiques (partagées en Visual Basic) :
Compare(t1,t2)
Méthodes/propriétés non statiques (Instance) :
Ticks, Milliseconds, Seconds, Hours, Days TotalMilliseconds, TotalSeconds, TotalMinutes, TotalHours, TotalDays, Equals, CompareTo(TimeSpan) Add(TimeSpan), Subtract(TimeSpan) Duration() [= ABS], Negate()
Non implémenté
ToString()
TimeSpan FromDay(Double), FromHours, all From Variants
TimeSpan Parse(String)
System.DateTime
Implémenté
Constructeurs :
DateTime(year, month, day) DateTime(year, month, day, hour, minutes, seconds) DateTime(year, month, day, hour, minutes, seconds, milliseconds)
Opérateurs:
Comparisons DateTime – DateTime (gives TimeSpan) DateTime + TimeSpan (gives DateTime) DateTime – TimeSpan (gives DateTime)
Méthodes statiques (partagées) :
Add(TimeSpan), AddTicks(Long), AddDays/Hours/Milliseconds/Minutes (Double) AddMonths/Years(Int32) Equals
Méthodes/propriétés non statiques (instance) :
Day, Month, Year, Hour, Minute, Second, Millisecond, DayOfWeek CompareTo(DateTime) TimeOfDay() Equals ToString()
Différence par rapport à .NET
Les valeurs datetime de SQL étant arrondies à .000, .003 ou .007 secondes, elle est moins précise que celles de .NET.
La plage de dateheure de SQL commence le 1er janvier 1753.
SQL n’a pas de type intégré pour TimeSpan. Il utilise différentes méthodes DATEDIFF qui retournent des entiers 32 bits. L’un est DATEDIFF(DAY,...), qui donne le nombre de jours ; un autre est DATEDIFF(MILLISECOND,...), qui donne le nombre de millisecondes. Une erreur se produit si les DateTimes sont séparés de plus de 24 jours. En revanche, .NET utilise des entiers 64 bits et mesure timeSpans dans des cases.
Pour se rapprocher autant que possible de la sémantique .NET dans SQL, LINQ to SQL convertit TimeSpans en entiers 64 bits et utilise les deux méthodes DATEDIFF mentionnées ci-dessus pour calculer le nombre de graduations entre deux dates.
Datetime
UtcNow est évalué sur le client lorsque la requête est traduite (comme toute expression qui n’implique pas de données de base de données).
Non implémenté
IsDaylightSavingTime()
IsLeapYear(Int32)
DaysInMonth(Int32, Int32)
ToBinary()
ToFileTime()
ToFileTimeUtc()
ToLongDateString()
ToLongTimeString()
ToOADate()
ToShortDateString()
ToShortTimeString()
ToUniversalTime()
FromBinary(Long), FileTime, FileTimeUtc, OADate
GetDateTimeFormats(...)
constructor DateTime(Long)
Parse(String)
DayOfYear
Prise en charge du débogage
DataContext fournit des méthodes et des propriétés pour obtenir le SQL généré pour les requêtes et le traitement des modifications. Ces méthodes peuvent être utiles pour comprendre LINQ to SQL fonctionnalités et pour déboguer des problèmes spécifiques.
Méthodes DataContext pour obtenir un SQL généré
Membre | Objectif |
---|---|
Journal | Imprime SQL avant son exécution. Couvre les commandes de requête, d’insertion, de mise à jour et de suppression. Usage : C#
Visual Basic
|
GetQueryText(query) | Retourne le texte de la requête sans l’exécuter. Usage : C# Console.WriteLine(db.GetQueryText(db.Customers));
Visual Basic
|
GetChangeText() | Retourne le texte des commandes SQL pour insérer/mettre à jour/supprimer sans les exécuter. Usage : C# Console.WriteLine(db.GetChangeText());
Visual Basic
|