LINQ to SQL: LINQ (Language-Integrated Query) .NET per dati relazionali

 

Dinesh Kulkarni, Luca Bologna, Matt Warren, Anders Hejlsberg, Kit George

Marzo 2007

Si applica a:
   Nome di Visual Studio Code "Orcas"
   .NET Framework 3.5

Riepilogo: LINQ to SQL fornisce un'infrastruttura di runtime per la gestione dei dati relazionali come oggetti senza perdere la possibilità di eseguire query. L'applicazione è libera di modificare gli oggetti mentre LINQ to SQL rimane nel rilevamento automatico delle modifiche in background. (119 pagine stampate)

Contenuto

Introduzione
Un tour rapido
   Creazione di classi di entità
   The DataContext
   Definizione delle relazioni
   Esecuzione di query tra relazioni
   Modifica e salvataggio di entità
Query In-Depth
   Esecuzione di query
   Identità dell'oggetto
   Relazioni
   Join
   Proiezioni
   Query compilate
   Traduzione SQL
Ciclo di vita dell'entità
   Rilevamento di modifiche
   Invio di modifiche
   Modifiche simultanee
   Transazioni
   Stored procedure
Classi di entità In-Depth
   Uso degli attributi
   Coerenza del grafico
   Notifiche di modifica
   Ereditarietà
Argomenti avanzati
   Creazione di database
   Interoperabilità con ADO.NET
   Modifica risoluzione dei conflitti
   Chiamata alle stored procedure
   Strumento generatore di classi di entità
   Informazioni di riferimento su DBML dello strumento generatore
   Entità a più livelli
   Mapping esterno
   Supporto e note per le funzioni di NET Framework
   Supporto per il debug

Introduzione

La maggior parte dei programmi scritti oggi modifica i dati in un modo o un altro e spesso questi dati vengono archiviati in un database relazionale. Tuttavia, esiste un'enorme divisione tra linguaggi di programmazione moderni e database nel modo in cui rappresentano e manipolano le informazioni. Questa mancata corrispondenza dell'impedenza è visibile in più modi. La maggior parte importante è che i linguaggi di programmazione accedono alle informazioni nei database tramite LE API che richiedono che le query vengano specificate come stringhe di testo. Queste query sono parti significative della logica del programma. Tuttavia, sono opachi per il linguaggio, non è in grado di trarre vantaggio dalla verifica in fase di compilazione e dalle funzionalità in fase di progettazione, ad esempio IntelliSense.

Naturalmente, le differenze vanno molto più profonde di quella. Come vengono rappresentate le informazioni, ovvero il modello di dati, è molto diverso tra i due. I linguaggi di programmazione moderni definiscono le informazioni sotto forma di oggetti. I database relazionali usano righe. Gli oggetti hanno un'identità univoca perché ogni istanza è fisicamente diversa da un'altra. Le righe vengono identificate dai valori chiave primaria. Gli oggetti hanno riferimenti che identificano e collegano le istanze insieme. Le righe vengono lasciate intenzionalmente distinte che richiedono che le righe correlate vengano collegate in modo libero usando chiavi esterne. Gli oggetti sono autonomi, esistenti purché siano ancora a cui fa riferimento un altro oggetto. Le righe esistono come elementi di tabelle, svanire non appena vengono rimosse.

Non ci si chiede che le applicazioni previste per colmare questo gap siano difficili da compilare e gestire. È certamente possibile semplificare l'equazione per liberarsi di un lato o dell'altro. Tuttavia, i database relazionali forniscono un'infrastruttura critica per l'archiviazione e l'elaborazione di query a lungo termine e i linguaggi di programmazione moderni sono indispensabili per lo sviluppo agile e il calcolo avanzato.

Fino a questo momento, è stato il lavoro dello sviluppatore dell'applicazione per risolvere questa mancata corrispondenza in ogni applicazione separatamente. Le soluzioni migliori finora sono state elaborate livelli di astrazione del database che traggono le informazioni tra i modelli a oggetti specifici del dominio delle applicazioni e la rappresentazione tabulare del database, rimodellando e riformattando i dati ogni modo. Tuttavia, oscurando l'origine dati vera, queste soluzioni finiscono per generare la funzionalità più interessante dei database relazionali; possibilità di eseguire query sui dati.

LINQ to SQL, un componente di Visual Studio Code Name "Orcas", fornisce un'infrastruttura di runtime per la gestione dei dati relazionali come oggetti senza perdere la possibilità di eseguire query. Esegue questa operazione traducendo query integrate nel linguaggio in SQL per l'esecuzione dal database e quindi traducendo i risultati tabulari in oggetti definiti. L'applicazione è quindi libera di modificare gli oggetti mentre LINQ to SQL rimane nel rilevamento automatico delle modifiche in background.

  • LINQ to SQL è progettato per essere non intrusivo all'applicazione.
    • È possibile eseguire la migrazione delle soluzioni di ADO.NET correnti a LINQ to SQL in modo frammentale (condivisione delle stesse connessioni e transazioni) poiché LINQ to SQL è semplicemente un altro componente della famiglia di ADO.NET. LINQ to SQL ha anche un ampio supporto per le stored procedure, consentendo il riutilizzo degli asset aziendali esistenti.
  • LINQ to SQL applicazioni sono facili da iniziare.
    • Gli oggetti collegati ai dati relazionali possono essere definiti proprio come gli oggetti normali, decorati solo con attributi per identificare il modo in cui le proprietà corrispondono alle colonne. Naturalmente, non è nemmeno necessario farlo a mano. Viene fornito uno strumento in fase di progettazione per automatizzare la conversione di schemi di database relazionali preesistenti in definizioni di oggetti.

Insieme, l'infrastruttura di runtime e gli strumenti in fase di progettazione LINQ to SQL riducono significativamente il carico di lavoro per lo sviluppatore di applicazioni di database. I capitoli seguenti forniscono una panoramica del modo in cui è possibile usare LINQ to SQL per eseguire attività comuni correlate al database. Si presuppone che il lettore abbia familiarità con Language-Integrated Query e gli operatori di query standard.

LINQ to SQL è un linguaggio agnostico. Qualsiasi linguaggio compilato per fornire Language-Integrated Query può usarlo per abilitare l'accesso alle informazioni archiviate nei database relazionali. Gli esempi in questo documento sono visualizzati sia in C# che in Visual Basic; LINQ to SQL può essere usato anche con la versione abilitata per LINQ del compilatore Visual Basic.

Un tour rapido

Il primo passaggio nella creazione di un'applicazione LINQ to SQL dichiara le classi oggetto che si userà per rappresentare i dati dell'applicazione. Di seguito viene illustrato un esempio.

Creazione di classi di entità

Si inizierà con una classe semplice Customer e la si associa alla tabella clienti nel database di esempio Northwind. A tale scopo, è necessario applicare solo un attributo personalizzato alla parte superiore della dichiarazione di classe. LINQ to SQL definisce l'attributo Table per questo scopo.

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'attributo Table ha una proprietà Name che è possibile usare per specificare il nome esatto della tabella di database. Se non viene fornita alcuna proprietà Name, LINQ to SQL presuppone che la tabella di database abbia lo stesso nome della classe. Solo le istanze delle classi dichiarate come tabelle verranno archiviate nel database. Le istanze di questi tipi di classi sono note come entità. Le classi stesse sono note come classi di entità.

Oltre all'associazione di classi alle tabelle, sarà necessario indicare ogni campo o proprietà che si intende associare a una colonna di database. A questo scopo, LINQ to SQL definisce l'attributo 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'attributo Column include diverse proprietà che è possibile usare per personalizzare il mapping esatto tra i campi e le colonne del database. Una proprietà di nota è la proprietà Id . Indica LINQ to SQL che la colonna del database fa parte della chiave primaria nella tabella.

Come per l'attributo Table , è necessario specificare informazioni nell'attributo Column se è diverso da quello che può essere dedotto dalla dichiarazione di campo o proprietà. In questo esempio è necessario indicare LINQ to SQL che il campo CustomerID fa parte della chiave primaria nella tabella, ma non è necessario specificare il nome o il tipo esatto.

Solo i campi e le proprietà dichiarati come colonne verranno mantenuti o recuperati dal database. Altri verranno considerati come parti temporanee della logica dell'applicazione.

The DataContext

DataContext è il canale principale in base al quale si recuperano oggetti dal database e rimettere le modifiche. Lo si usa nello stesso modo in cui si userebbe una connessione ADO.NET. In effetti, DataContext viene inizializzato con una connessione o una stringa di connessione fornita. Lo scopo di DataContext consiste nel tradurre le richieste di oggetti in query SQL eseguite nel database e quindi assemblare oggetti dai risultati. DataContext abilita la query integrata nel linguaggio implementando lo stesso modello di operatore degli operatori di query standard, ad esempio Where e Select.

Ad esempio, è possibile usare DataContext per recuperare gli oggetti dei clienti la cui città è Londra come indicato di seguito:

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

Ogni tabella di database è rappresentata come raccolta Table , accessibile tramite il metodo GetTable() usando la classe di entità per identificarla. È consigliabile dichiarare un oggetto DataContext fortemente tipizzato anziché basarsi sulla classe DataContext di base e sul metodo GetTable(). Un dataContext fortemente tipizzato dichiara tutte le raccolte Table come membri del contesto.

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 query per i clienti di Londra può quindi essere espressa più semplicemente come:

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

Continuerà a usare la classe Northwind fortemente tipizzata per il resto del documento di panoramica.

Definizione delle relazioni

Le relazioni nei database relazionali vengono in genere modellate come valori chiave esterna che fanno riferimento alle chiavi primarie in altre tabelle. Per spostarsi tra di essi, è necessario unire in modo esplicito le due tabelle usando un'operazione di join relazionale. Gli oggetti, d'altra parte, fanno riferimento tra loro usando riferimenti alle proprietà o raccolte di riferimenti spostati usando la notazione "dot". Ovviamente, la punteggiatura è più semplice rispetto all'aggiunta, poiché non è necessario ricordare la condizione di join esplicita ogni volta che si passa.

Per le relazioni di dati come queste che saranno sempre uguali, diventa molto pratico codificarli come riferimenti alle proprietà nella classe di entità. LINQ to SQL definisce un attributo Association che è possibile applicare a un membro utilizzato per rappresentare una relazione. Una relazione di associazione è un tipo simile a una relazione chiave esterna a chiave primaria creata da valori di colonna corrispondenti tra tabelle.

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 ha ora una proprietà che dichiara la relazione tra i clienti e i relativi ordini. La proprietà Orders è di tipo EntitySet perché la relazione è uno-a-molti. Viene usata la proprietà OtherKey nell'attributo Association per descrivere come viene eseguita questa associazione. Specifica i nomi delle proprietà della classe correlata da confrontare con questa. Non è stata specificata anche una proprietà ThisKey . In genere, verrà usato per elencare i membri a questo lato della relazione. Tuttavia, omettendolo, è possibile LINQ to SQL dedurrli dai membri che costituiscono la chiave primaria.

Si noti come questa operazione viene invertita nella definizione per 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 usa il tipo EntityRef per descrivere nuovamente la relazione con il cliente. L'uso della classe EntityRef è necessario per supportare il caricamento posticipato (illustrato più avanti). L'attributo Association per la proprietà Customer specifica la proprietà ThisKey poiché i membri non inferibili sono ora a questo lato della relazione.

Esaminare anche la proprietà Storage . Indica LINQ to SQL quale membro privato viene usato per contenere il valore della proprietà. Ciò consente LINQ to SQL di ignorare le funzioni di accesso alle proprietà pubbliche quando archivia e recupera il valore. Questo è essenziale se si vuole LINQ to SQL evitare qualsiasi logica di business personalizzata scritta nelle funzioni di accesso. Se la proprietà di archiviazione non è specificata, verranno usate invece le funzioni di accesso pubbliche. È anche possibile usare la proprietà Storage con attributi Column .

Dopo aver introdotto relazioni nelle classi di entità, la quantità di codice da scrivere aumenta man mano che si introduce il supporto per le notifiche e la coerenza del grafo. Fortunatamente, esiste uno strumento (descritto più avanti) che può essere usato per generare tutte le definizioni necessarie come classi parziali, consentendo di usare una combinazione di codice generato e logica di business personalizzata.

Per il resto di questo documento, si presuppone che lo strumento sia stato usato per generare un contesto di dati Northwind completo e tutte le classi di entità.

Esecuzione di query tra relazioni

Ora che si hanno relazioni, è possibile usarle quando si scrivono query semplicemente facendo riferimento alle proprietà delle relazioni definite nella 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 query precedente usa la proprietà Orders per formare il prodotto incrociato tra i clienti e gli ordini, generando una nuova sequenza di coppie Customer e Order .

È anche possibile eseguire il contrario.

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

In questo esempio, gli ordini vengono sottoposti a query e la relazione Customer viene usata per accedere alle informazioni sull'oggetto Customer associato.

Modifica e salvataggio di entità

Alcune applicazioni vengono compilate solo con query in mente. Anche i dati devono essere creati e modificati. LINQ to SQL è progettato per offrire massima flessibilità nel modificare e rendere persistenti le modifiche apportate agli oggetti. Non appena sono disponibili oggetti di entità, recuperandoli tramite una query o creandoli di nuovo, è possibile modificarli come oggetti normali nell'applicazione, modificarne i valori o aggiungerli e rimuoverli dalle raccolte in base alle esigenze. LINQ to SQL tiene traccia di tutte le modifiche e è pronto per trasmetterli nuovamente al database non appena si esegue l'operazione.

L'esempio seguente usa le classi Customer and Order generate da uno strumento dai metadati dell'intero database di esempio Northwind. Le definizioni della classe non sono state visualizzate per brevità.

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()

Quando SubmitChanges() viene chiamato, LINQ to SQL genera automaticamente ed esegue comandi SQL per trasmettere le modifiche al database. È anche possibile eseguire l'override di questo comportamento con la logica personalizzata. La logica personalizzata può chiamare una stored procedure del database.

Query In-Depth

LINQ to SQL fornisce un'implementazione degli operatori di query standard per gli oggetti associati alle tabelle in un database relazionale. Questo capitolo descrive gli aspetti specifici LINQ to SQL delle query.

Esecuzione di query

Se si scrive una query come espressione di query di alto livello o si compila uno dei singoli operatori, la query scritta non è un'istruzione imperativa eseguita immediatamente. È una descrizione. Ad esempio, nella dichiarazione seguente la variabile locale q fa riferimento alla descrizione della query non al risultato dell'esecuzione.

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

Il tipo effettivo di q in questa istanza è IQueryable<Customer>. Non è fino a quando l'applicazione tenta di enumerare il contenuto della query effettivamente eseguita. In questo esempio l'istruzione foreach causa l'esecuzione.

Un oggetto IQueryable è simile a un oggetto comando ADO.NET. L'esecuzione di una query non implica che una query sia stata eseguita. Un oggetto comando viene inserito in una stringa che descrive una query. Analogamente, un oggetto IQueryable contiene una descrizione di una query codificata come struttura di dati nota come espressione. Un oggetto comando ha un metodo ExecuteReader() che causa l'esecuzione, restituendo risultati come DataReader. Un oggetto IQueryable ha un metodo GetEnumerator() che causa l'esecuzione, restituendo risultati come cliente> IEnumerator<.

Di conseguenza, se una query viene enumerata due volte, verrà eseguita due volte.

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

Questo comportamento è noto come esecuzione posticipata. Proprio come con un oggetto comando ADO.NET è possibile conservare una query ed eseguirlo di nuovo.

Naturalmente, gli autori di applicazioni spesso devono essere molto espliciti su dove e quando viene eseguita una query. Sarebbe imprevisto se un'applicazione eseguisse una query più volte semplicemente perché era necessario esaminare i risultati più volte. Ad esempio, è possibile associare i risultati di una query a un elemento simile a un datagrid. Il controllo può enumerare i risultati ogni volta che disegna sullo schermo.

Per evitare l'esecuzione più volte, convertire i risultati in un numero qualsiasi di classi di raccolta standard. È facile convertire i risultati in un elenco o in una matrice usando gli operatori di query standard ToList() o ToArray().It is easy to convert the results into a list or array using the standard query operators ToList() or 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 

Un vantaggio dell'esecuzione posticipata è che le query possono essere costruite in modo a fasi con l'esecuzione solo al termine della costruzione. È possibile iniziare a comporre una parte di una query, assegnarla a una variabile locale e successivamente continuare ad applicare altri operatori.

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 

In questo esempio, q inizia come query per tutti i clienti di Londra. Successivamente cambia in una query ordinata a seconda dello stato dell'applicazione. Rinviando l'esecuzione, la query può essere costruita in base alle esigenze esatte dell'applicazione senza richiedere la manipolazione di stringhe rischiose.

Identità dell'oggetto

Gli oggetti nel runtime hanno un'identità univoca. Se due variabili fanno riferimento allo stesso oggetto, fanno effettivamente riferimento alla stessa istanza dell'oggetto. Per questo motivo, le modifiche apportate tramite un percorso attraverso una variabile sono immediatamente visibili tramite l'altra. Le righe in una tabella di database relazionale non hanno un'identità univoca. Tuttavia, hanno una chiave primaria e tale chiave primaria può essere univoca, ovvero nessuna riga può condividere la stessa chiave. Tuttavia, questo limita solo il contenuto della tabella di database. Pertanto, purché si interagisca solo con i dati tramite comandi remoti, si tratta di circa la stessa cosa.

Tuttavia, questo è raramente il caso. La maggior parte dei casi i dati vengono portati fuori dal database e in un livello diverso in cui un'applicazione lo modifica. Chiaramente, questo è il modello che LINQ to SQL è progettato per supportare. Quando i dati vengono portati fuori dal database come righe, non è previsto che due righe che rappresentano gli stessi dati corrispondano effettivamente alle stesse istanze di riga. Se si esegue una query per un cliente specifico due volte, si ottengono due righe di dati, ognuna contenente le stesse informazioni.

Tuttavia, con gli oggetti, ci si aspetta qualcosa di molto diverso. Si prevede che, se si chiede nuovamente a DataContext le stesse informazioni, verrà restituita la stessa istanza dell'oggetto. Questo comportamento è previsto perché gli oggetti hanno un significato speciale per l'applicazione e si prevede che si comportino come oggetti normali. Sono stati progettati come gerarchie o gragrafi e si prevede di recuperarli come tali, senza orde di istanze replicate semplicemente perché è stata richiesta la stessa cosa due volte.

Per questo motivo, DataContext gestisce l'identità dell'oggetto. Ogni volta che viene recuperata una nuova riga dal database, viene registrata in una tabella Identity tramite la chiave primaria e viene creato un nuovo oggetto. Ogni volta che la stessa riga viene recuperata nuovamente, l'istanza dell'oggetto originale viene restituita all'applicazione. In questo modo, DataContext converte il concetto di identità (chiavi) dei database nel concetto di linguaggio (istanze). L'applicazione vede l'oggetto solo nello stato in cui è stato recuperato per la prima volta. I nuovi dati, se diversi, vengono buttati via.

È possibile che questo sia un problema, perché qualsiasi applicazione potrebbe buttare via i dati? Come si scopre che LINQ to SQL gestisce l'integrità degli oggetti locali ed è in grado di supportare gli aggiornamenti ottimistici. Poiché le uniche modifiche apportate dopo la creazione iniziale dell'oggetto sono quelle apportate dall'applicazione, la finalità dell'applicazione è chiara. Se le modifiche apportate da una parte esterna si sono verificate nel frattempo, verranno identificate al momento della chiamata a SubmitChanges(). Altre informazioni sono descritte nella sezione Modifiche simultanee.

Si noti che, nel caso in cui il database contenga una tabella senza una chiave primaria, LINQ to SQL consente l'invio delle query sulla tabella, ma non consente gli aggiornamenti. Ciò è dovuto al fatto che il framework non è in grado di identificare la riga da aggiornare a causa della mancanza di una chiave univoca.

Naturalmente, se l'oggetto richiesto dalla query è facilmente identificabile dalla relativa chiave primaria perché non viene eseguita alcuna query già recuperata. La tabella Identity funge da cache che archivia tutti gli oggetti recuperati in precedenza.

Relazioni

Come illustrato nella presentazione rapida, i riferimenti ad altri oggetti o raccolte di altri oggetti nelle definizioni di classe corrispondono direttamente alle relazioni di chiave esterna nel database. È possibile usare queste relazioni quando si esegue una query semplicemente usando la notazione punto per accedere alle proprietà della relazione, passando da un oggetto a un altro. Queste operazioni di accesso si traducono in join più complessi o in query secondarie correlate nell'equivalente SQL, consentendo di esaminare l'oggetto grafico durante una query. Ad esempio, usando la query seguente è possibile spostarsi da ordini a clienti per limitare i risultati solo agli ordini per i clienti residenti nell'area londinese.

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"

Se le proprietà della relazione non esistono, è necessario scriverle manualmente come join esattamente come si farebbe in una query 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 proprietà di relazione consente di definire questa relazione specifica una volta che si abilita l'uso della sintassi dei punti più comoda. Tuttavia, questo non è il motivo per cui esistono proprietà di relazione. Esistono perché si tende a definire i modelli a oggetti specifici del dominio come gerarchie o gragrafi. Gli oggetti su cui si sceglie di programmare hanno riferimenti ad altri oggetti. È solo una coincidenza felice che poiché le relazioni tra oggetti corrispondono alle relazioni di stile di chiave esterna nei database che l'accesso alle proprietà porta a un modo pratico per scrivere join.

Pertanto, l'esistenza di proprietà di relazione è più importante sul lato dei risultati di una query rispetto all'interno della query stessa. Dopo aver ottenuto le mani su un cliente specifico, la definizione della classe indica che i clienti hanno ordini. Pertanto, quando si esamina la proprietà Orders di un cliente specifico, si prevede di visualizzare la raccolta popolata con tutti gli ordini del cliente, poiché questo è in realtà il contratto dichiarato definendo le classi in questo modo. Ci si aspetta di vedere gli ordini lì anche se non hai chiesto in particolare ordini in anticipo. Si prevede che il modello a oggetti mantenga un'illusione che si tratti di un'estensione in memoria del database, con oggetti correlati immediatamente disponibili.

LINQ to SQL implementa una tecnica denominata caricamento posticipato per contribuire a mantenere questa illusione. Quando si esegue una query per un oggetto, in realtà si recuperano solo gli oggetti richiesti. Gli oggetti correlati non vengono recuperati contemporaneamente in modo automatico. Tuttavia, il fatto che gli oggetti correlati non sono già caricati non è osservabile perché non appena si tenta di accedervi una richiesta viene interrotta per recuperarli.

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

Ad esempio, è possibile eseguire una query per un determinato set di ordini e quindi inviare una notifica tramite posta elettronica solo occasionalmente a determinati clienti. Non sarebbe necessario recuperare tutti i dati dei clienti in anticipo con ogni ordine. Il caricamento posticipato consente di rinviare il costo del recupero di informazioni aggiuntive fino a quando non è assolutamente necessario.

Naturalmente, l'opposto potrebbe anche essere vero. Potrebbe essere disponibile un'applicazione che deve esaminare i dati dei clienti e degli ordini contemporaneamente. È noto che sono necessari entrambi i set di dati Si sa che l'applicazione eseguirà il drill-down degli ordini di ogni cliente non appena vengono visualizzati. Sarebbe sfortunato attivare singole query per gli ordini per ogni cliente. Ciò che si vuole veramente accadere è avere i dati dell'ordine recuperati insieme ai clienti.

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

Certamente, è sempre possibile trovare un modo per unire i clienti e gli ordini in una query formando il prodotto incrociato e recuperando tutti i bit relativi di dati come una grande proiezione. Ma i risultati non sarebbero entità. Le entità sono oggetti con identità che è possibile modificare mentre i risultati sono proiezioni che non possono essere modificate e rese persistenti. Peggio, si sta recuperando una grande quantità di dati ridondanti perché ogni cliente ripete per ogni ordine nell'output di join flat.

Ciò che serve è un modo per recuperare contemporaneamente un set di oggetti correlati, ovvero una parte delineata di un grafico in modo da non recuperare mai più o meno di quanto necessario per l'uso previsto.

LINQ to SQL consente di richiedere il caricamento immediato di un'area del modello a oggetti solo per questo motivo. A tale scopo, è possibile specificare un oggetto DataShape per un oggetto DataContext. La classe DataShape viene utilizzata per indicare al framework quali oggetti recuperare quando viene recuperato un particolare tipo. A tale scopo, utilizzare il metodo LoadWith come illustrato di seguito:

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

Nella query precedente tutti gli Ordini per tutti i clienti che vivono a Londra vengono recuperati quando viene eseguita la query, in modo che l'accesso successivo alla proprietà Orders in un oggetto Customer non attiva una query di database.

La classe DataShape può essere usata anche per specificare le sotto query applicate a una relazione di spostamento. Ad esempio, se si desidera recuperare solo gli Ordini che sono stati spediti oggi, è possibile usare il metodo AssociateWith in DataShape come illustrato di seguito:

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

Nel codice precedente, l'istruzione foreach interna esegue l'iterazione solo sugli Ordini che sono stati spediti oggi, perché solo tali ordini sono stati recuperati dal database.

È importante notare due fatti sulla classe DataShape :

  1. Dopo aver assegnato un oggetto DataShape a un oggetto DataContext, DataShape non può essere modificato. Qualsiasi chiamata al metodo LoadWith o AssociateWith in tale oggetto DataShape restituirà un errore in fase di esecuzione.

  2. Non è possibile creare cicli usando LoadWith o AssociateWith. Ad esempio, il seguente genera un errore in fase di esecuzione:

    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)
    

Join

La maggior parte delle query sui modelli a oggetti si basa principalmente sull'esplorazione dei riferimenti agli oggetti nel modello a oggetti. Esistono tuttavia interessanti "relazioni" tra entità che potrebbero non essere acquisite nel modello a oggetti come riferimenti. Ad esempio , Customer.Orders è una relazione utile basata sulle relazioni di chiave esterna nel database Northwind. Tuttavia, i fornitori e i clienti nella stessa città o paese sono una relazione ad hoc che non si basa su una relazione di chiave esterna e potrebbe non essere acquisita nel modello a oggetti. I join forniscono un meccanismo aggiuntivo per gestire tali relazioni. LINQ to SQL supporta i nuovi operatori di join introdotti in LINQ.

Prendere in considerazione il problema seguente: trovare i fornitori e i clienti che si trovano nella stessa città. La query seguente restituisce i nomi dei fornitori e delle società dei clienti e la città comune come risultato flat. Si tratta dell'equivalente dell'equi-join interno nei database relazionali:

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 query precedente elimina i fornitori che non si trovano nella stessa città di un determinato cliente. Tuttavia, ci sono momenti in cui non si vuole eliminare una delle entità in una relazione ad hoc . La query seguente elenca tutti i fornitori con gruppi di clienti per ognuno dei fornitori. Se un determinato fornitore non ha alcun cliente nella stessa città, il risultato è una raccolta vuota di clienti corrispondenti a tale fornitore. Si noti che i risultati non sono flat: ogni fornitore ha una raccolta associata. In modo efficace, questo consente di unire due sequenze e gruppi di elementi della seconda sequenza dagli elementi della prima sequenza.

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

Il join di gruppo può essere esteso anche a più raccolte. La query seguente estende la query precedente elencando i dipendenti che si trovano nella stessa città del fornitore. In questo caso, il risultato mostra un fornitore con raccolte (possibilmente vuote) di clienti e dipendenti.

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

I risultati di un join di gruppo possono anche essere appiattiti. I risultati dell'appiattimento del gruppo tra i fornitori e i clienti sono più voci per i fornitori con più clienti nella loro città, uno per cliente. Le raccolte vuote vengono sostituite con valori Null. Equivale a un equi-join esterno sinistro nei database relazionali.

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

Le firme per gli operatori di join sottostanti sono definite nel documento degli operatori di query standard. Sono supportati solo equi-join e i due operandi di uguale devono avere lo stesso tipo.

Proiezioni

Finora sono state esaminate solo le query per il recupero di entità, ovvero gli oggetti direttamente associati alle tabelle di database. Non dobbiamo limitarci a questo. La bellezza di un linguaggio di query è che è possibile recuperare informazioni in qualsiasi forma desiderata. Non sarà possibile sfruttare il rilevamento automatico delle modifiche o la gestione delle identità quando si esegue questa operazione. Tuttavia, è possibile ottenere solo i dati desiderati.

Ad esempio, potrebbe essere sufficiente conoscere i nomi aziendali di tutti i clienti a Londra. Se si tratta del caso non esiste alcun motivo particolare per recuperare interi oggetti del cliente semplicemente per selezionare nomi. È possibile proiettare i nomi come parte della query.

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

In questo caso, q diventa una query che recupera una sequenza di stringhe.

Se si vuole tornare più di un solo nome, ma non sufficiente per giustificare il recupero dell'intero oggetto cliente, è possibile specificare qualsiasi subset desiderato creando i risultati come parte della query.

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

In questo esempio viene usato un inizializzatore di oggetti anonimo per creare una struttura che contiene sia il nome della società che il numero di telefono. Non è possibile sapere cosa chiamare il tipo, ma con dichiarazione di variabile locale digitata in modo implicito nel linguaggio che non è necessariamente necessario.

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

Se si usano immediatamente i dati, i tipi anonimi consentono di definire in modo esplicito le classi per contenere i risultati della query.

È anche possibile formare prodotti incrociati di tutti gli oggetti, anche se si potrebbe raramente avere un motivo per farlo.

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

Questa query costruisce una sequenza di coppie di oggetti customer e order.

È anche possibile eseguire proiezioni in qualsiasi fase della query. È possibile proiettare i dati in oggetti appena costruiti e quindi fare riferimento ai membri di tali oggetti nelle operazioni di query successive.

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

Tuttavia, è consigliabile usare costruttori con parametri in questa fase. È tecnicamente valido farlo, ma è impossibile LINQ to SQL tenere traccia del modo in cui l'utilizzo del costruttore influisce sullo stato membro senza comprendere il codice effettivo all'interno del costruttore.

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

Poiché LINQ to SQL tenta di tradurre la query in tipi di oggetti relazionali puri definiti in locale non sono disponibili nel server per costruire effettivamente. Tutta la costruzione di oggetti viene effettivamente posticipata fino a quando i dati vengono recuperati dal database. Al posto dei costruttori effettivi, SQL generato usa la normale proiezione di colonna SQL. Poiché non è possibile che il traduttore di query comprenda cosa accade durante una chiamata al costruttore, non è in grado di stabilire un significato per il campo Nome di MyType.

La procedura consigliata consiste invece nell'usare sempre gli inizializzatori a oggetti per codificare le proiezioni.

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

L'unico luogo sicuro per usare un costruttore con parametri è nella proiezione finale di una query.

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>

È anche possibile usare l'annidamento elaborato dei costruttori di oggetti se si desidera, ad esempio questo esempio che costruisce XML direttamente dal risultato di una query. Funziona finché è l'ultima proiezione della query.

Tuttavia, anche se le chiamate del costruttore sono comprese, le chiamate ai metodi locali potrebbero non essere. Se la proiezione finale richiede la chiamata di metodi locali, è improbabile che LINQ to SQL sarà in grado di obbligare. Le chiamate al metodo che non hanno una traduzione nota in SQL non possono essere usate come parte della query. Un'eccezione a questa regola è chiamate di metodo che non hanno argomenti dipendenti dalle variabili di query. Questi non sono considerati parte della query tradotta e vengono invece considerati come parametri.

Le proiezioni ancora elaborate (trasformazioni) possono richiedere la logica procedurale locale per implementare. Per usare i propri metodi locali in una proiezione finale, sarà necessario proiettare due volte. La prima proiezione estrae tutti i valori di dati che sarà necessario fare riferimento e la seconda proiezione esegue la trasformazione. Tra queste due proiezioni è presente una chiamata all'operatore AsEnumerable() che sposta l'elaborazione in quel punto da una query LINQ to SQL in una query eseguita localmente.

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)}

Nota L'operatore AsEnumerable(), a differenza di ToList() e ToArray(), non causa l'esecuzione della query. È ancora rinviato. L'operatore AsEnumerable() modifica semplicemente la digitazione statica della query, trasformando un IQueryable<T> (IQueryable(ofT) in Visual Basic) in un oggetto IEnumerable T>(IEnumerable<(ofT) in Visual Basic, ingannando il compilatore nel trattare il resto della query come eseguito localmente.

Query compilate

In molte applicazioni è comune eseguire query strutturalmente simili molte volte. In questi casi, è possibile aumentare le prestazioni compilando la query una sola volta ed eseguendola più volte nell'applicazione con parametri diversi. Questo risultato viene ottenuto in LINQ to SQL usando la classe CompiledQuery. Il codice seguente illustra come definire una query compilata:

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

Il metodo Compile restituisce un delegato che può essere memorizzato nella cache ed eseguito più volte semplicemente modificando i parametri di input. Il codice seguente illustra un esempio di questo:

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

Traduzione SQL

LINQ to SQL non esegue effettivamente query, ma il database relazionale. LINQ to SQL converte le query scritte in query SQL equivalenti e le invia al server per l'elaborazione. Poiché l'esecuzione viene posticipata, LINQ to SQL è in grado di esaminare l'intera query anche se assemblata da più parti.

Poiché il server di database relazionale non esegue effettivamente IL (a parte l'integrazione CLR in SQL Server 2005), le query non vengono trasmesse al server come IL. Vengono infatti trasmessi come query SQL con parametri in formato testo.

Naturalmente, SQL, anche T-SQL con integrazione CLR, non è in grado di eseguire la varietà di metodi disponibili localmente per il programma. Pertanto, le query scritte devono essere convertite in operazioni e funzioni equivalenti disponibili all'interno dell'ambiente SQL.

La maggior parte dei metodi e degli operatori nei tipi predefiniti di .Net Framework include traduzioni dirette in SQL. Alcuni possono essere prodotti dalle funzioni disponibili. Quelli che non possono essere tradotti non sono consentiti, generando eccezioni di runtime se si tenta di usarle. È disponibile una sezione più avanti nel documento che illustra in dettaglio i metodi del framework implementati per la conversione in SQL.

Ciclo di vita dell'entità

LINQ to SQL è più di un'implementazione degli operatori di query standard per i database relazionali. Oltre a tradurre le query, si tratta di un servizio che gestisce gli oggetti per tutta la durata, consentendo di mantenere l'integrità dei dati e di automatizzare il processo di conversione delle modifiche nell'archivio.

In uno scenario tipico, gli oggetti vengono recuperati tramite una o più query e quindi modificati in qualche modo o in un altro modo fino a quando l'applicazione non è pronta per inviare le modifiche al server. Questo processo può ripetere un numero di volte fino a quando l'applicazione non ha più usato per queste informazioni. A questo punto, gli oggetti vengono recuperati dal runtime esattamente come gli oggetti normali. I dati, tuttavia, rimangono nel database. Anche dopo essere stata cancellata dall'esistenza in fase di esecuzione, gli oggetti che rappresentano gli stessi dati possono comunque essere recuperati. In questo senso, la vera durata dell'oggetto esiste oltre ogni singola manifestazione in fase di esecuzione.

L'obiettivo di questo capitolo è il ciclo di vita dell'entità in cui un ciclo fa riferimento all'intervallo di tempo di una singola manifestazione di un oggetto entità all'interno di un contesto di runtime specifico. Il ciclo inizia quando DataContext diventa a conoscenza di una nuova istanza e termina quando l'oggetto o DataContext non è più necessario.

Rilevamento di modifiche

Dopo che le entità vengono recuperate dal database, è possibile modificarle come si preferisce. Sono i tuoi oggetti; usarli come si farà. A tale scopo, LINQ to SQL tiene traccia delle modifiche in modo che possa renderle persistenti nel database quando viene chiamato SubmitChanges().

LINQ to SQL inizia a tenere traccia delle entità nel momento in cui vengono recuperate dal database, prima che le mani si trovino. In effetti, anche il servizio di gestione delle identità discusso in precedenza è già stato avviato. Il rilevamento delle modifiche costa molto poco in sovraccarico aggiuntivo fino a quando non si inizia effettivamente a apportare modifiche.

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"

Non appena viene assegnato CompanyName nell'esempio precedente, LINQ to SQL diventa a conoscenza della modifica ed è in grado di registrarlo. I valori originali di tutti i membri dati vengono conservati dal servizio rilevamento modifiche.

Il servizio rilevamento modifiche registra anche tutte le manipolazioni delle proprietà delle relazioni. Si usano le proprietà delle relazioni per stabilire i collegamenti tra le entità, anche se possono essere collegati da valori di chiave nel database. Non è necessario modificare direttamente i membri associati alle colonne chiave. LINQ to SQL li sincronizza automaticamente prima dell'invio delle modifiche.

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

È possibile spostare gli ordini da un cliente a un altro semplicemente effettuando un'assegnazione alla proprietà Customer . Poiché la relazione esiste tra il cliente e l'ordine, è possibile modificare la relazione modificando entrambi i lati. È possibile rimuoverli facilmente dalla raccolta Orders di cust2 e aggiungerli alla raccolta ordini di cust1, come illustrato di seguito.

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)

Naturalmente, se si assegna una relazione il valore di Null, in realtà si sta liberando completamente della relazione. L'assegnazione di una proprietà Customer di un ordine a Null rimuove effettivamente l'ordine dall'elenco del cliente.

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))

L'aggiornamento automatico di entrambi i lati di una relazione è essenziale per mantenere la coerenza del grafico degli oggetti. A differenza degli oggetti normali, le relazioni tra i dati sono spesso bidirezionali. LINQ to SQL consente di usare le proprietà per rappresentare le relazioni. Tuttavia, non offre un servizio per mantenere sincronizzate automaticamente queste proprietà bidirezionali. Si tratta di un livello di servizio che deve essere inserito direttamente nelle definizioni di classe. Le classi di entità generate usando lo strumento di generazione del codice hanno questa funzionalità. Nel capitolo successivo verrà illustrato come eseguire questa operazione alle proprie classi scritte a mano.

È tuttavia importante notare che la rimozione di una relazione non implica l'eliminazione di un oggetto dal database. Tenere presente che la durata dei dati sottostanti viene mantenuta nel database fino a quando la riga non viene eliminata dalla tabella. L'unico modo per eliminare effettivamente un oggetto consiste nel rimuoverlo dalla raccolta 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)

Come per tutte le altre modifiche, l'ordine non è stato effettivamente eliminato. Sembra proprio così da quando è stato rimosso e scollegato dal resto degli oggetti. Quando l'oggetto order è stato rimosso dalla tabella Orders , è stato contrassegnato per l'eliminazione dal servizio rilevamento modifiche. L'eliminazione effettiva dal database si verificherà quando le modifiche vengono inviate in una chiamata a SubmitChanges(). Si noti che l'oggetto stesso non viene mai eliminato. Il runtime gestisce la durata delle istanze dell'oggetto, quindi rimane in giro finché si mantiene ancora un riferimento. Tuttavia, dopo che un oggetto è stato rimosso dalla tabella e le modifiche inviate non vengono più rilevate dal servizio rilevamento modifiche.

L'unica volta che un'entità viene lasciata non rilevata è quando esiste prima che DataContext ne sia a conoscenza. Ciò si verifica ogni volta che si creano nuovi oggetti nel codice. È possibile usare istanze di classi di entità nell'applicazione senza mai recuperarle da un database. La modifica della gestione delle identità e della modifica si applica solo agli oggetti di cui DataContext è a conoscenza. Pertanto, nessun servizio è abilitato per le istanze appena create fino a quando non vengono aggiunte a DataContext.

Questa situazione può verificarsi in uno dei due modi seguenti. È possibile chiamare manualmente il metodo Add() nell'insieme Table correlato.

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)

In alternativa, è possibile collegare una nuova istanza a un oggetto di cui DataContext è già a conoscenza.

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 rileverà le nuove istanze dell'oggetto anche se sono collegate ad altre nuove istanze.

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 
      }
   } )

In pratica, DataContext riconoscerà qualsiasi entità nel grafico a oggetti attualmente non rilevata come nuova istanza, indipendentemente dal fatto che sia stato chiamato il metodo Add().

Uso di un datacontext di sola lettura

Molti scenari non richiedono l'aggiornamento delle entità recuperate dal database. La visualizzazione di una tabella di Clienti in una pagina Web è un esempio ovvio. In tutti questi casi, è possibile migliorare le prestazioni indicare a DataContext di non tenere traccia delle modifiche apportate alle entità. Ciò viene ottenuto specificando la proprietà ObjectTracking in DataContext da false come nel codice seguente:

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

Invio di modifiche

Indipendentemente dal numero di modifiche apportate agli oggetti, tali modifiche sono state apportate solo alle repliche in memoria. Non è ancora successo ai dati effettivi nel database. La trasmissione di queste informazioni al server non verrà eseguita finché non verrà richiesta in modo esplicito chiamando SubmitChanges() nel 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()

Quando si chiama SubmitChanges(), DataContext tenterà di tradurre tutte le modifiche in comandi SQL equivalenti, inserimento, aggiornamento o eliminazione di righe nelle tabelle corrispondenti. Queste azioni possono essere sostituite dalla logica personalizzata se si desidera, tuttavia l'ordine di invio viene orchestrato da un servizio di DataContext noto come processore di modifiche.

La prima cosa che accade quando si chiama SubmitChanges() è che il set di oggetti noti viene esaminato per determinare se sono state associate nuove istanze. Queste nuove istanze vengono aggiunte al set di oggetti monitorati. Quindi, tutti gli oggetti con modifiche in sospeso vengono ordinati in una sequenza di oggetti in base alle dipendenze tra di essi. Tali oggetti i cui cambiamenti dipendono da altri oggetti vengono sequenziati dopo le relative dipendenze. I vincoli di chiave esterna e i vincoli di univocità nel database svolgono un ruolo importante nel determinare l'ordinamento corretto delle modifiche. Quindi, prima che vengano trasmesse modifiche effettive, viene avviata una transazione per incapsulare la serie di singoli comandi, a meno che non si trovi già nell'ambito. Infine, una per una delle modifiche apportate agli oggetti viene tradotta in comandi SQL e inviata al server.

A questo punto, eventuali errori rilevati dal database causeranno l'interruzione del processo di invio e verrà generata un'eccezione. Tutte le modifiche apportate al database verranno eseguito il rollback come se non fosse stato eseguito nessuno degli invii. DataContext avrà comunque una registrazione completa di tutte le modifiche in modo che sia possibile tentare di correggere il problema e inviarli nuovamente chiamando 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

Quando la transazione intorno all'invio viene completata correttamente, DataContext accetterà le modifiche agli oggetti semplicemente dimenticando le informazioni di rilevamento delle modifiche.

Modifiche simultanee

Esistono diversi motivi per cui una chiamata a SubmitChanges() potrebbe non riuscire. È possibile che sia stato creato un oggetto con una chiave primaria non valida; uno già in uso o con un valore che viola un vincolo di controllo del database. Questi tipi di controlli sono difficili da creare nella logica di business perché spesso richiedono una conoscenza assoluta dell'intero stato del database. Tuttavia, il motivo più probabile per l'errore è semplicemente che un altro utente ha apportato modifiche agli oggetti prima dell'utente.

Certamente, questo sarebbe impossibile se si bloccasse ogni oggetto nel database e si usa una transazione completamente serializzata. Tuttavia, questo stile di programmazione (concorrenza pessimistica) viene raramente usato poiché è costoso e vero conflitti raramente si verificano. La forma più popolare di gestione delle modifiche simultanee consiste nell'usare una forma di concorrenza ottimistica. In questo modello non vengono eseguiti blocchi rispetto alle righe del database. Ciò significa che qualsiasi numero di modifiche apportate al database potrebbe essere stato generato tra il momento in cui è stato recuperato prima gli oggetti e l'ora in cui sono state inviate le modifiche.

Pertanto, a meno che non si voglia andare con un criterio che l'ultimo aggiornamento vince, cancellare qualsiasi altra operazione si è verificata prima dell'utente, probabilmente si vuole essere avvisati del fatto che i dati sottostanti sono stati modificati da un altro utente.

DataContext ha il supporto predefinito per la concorrenza ottimistica rilevando automaticamente i conflitti di modifica. Gli aggiornamenti individuali hanno esito positivo solo se lo stato corrente del database corrisponde allo stato in cui sono stati compresi i dati da inserire quando sono stati recuperati prima gli oggetti. Ciò avviene in base a ogni oggetto, avvisando solo le violazioni se si verificano oggetti a cui sono state apportate modifiche.

È possibile controllare il grado in cui DataContext rileva conflitti di modifica quando si definiscono le classi di entità. Ogni attributo Column ha una proprietà denominata UpdateCheck che può essere assegnata a uno dei tre valori: Always, Never e WhenChanged. Se non si imposta l'impostazione predefinita per un attributo Column è Always, ovvero i valori di dati rappresentati da tale membro vengono sempre controllati per i conflitti, ovvero, a meno che non esista un tie-breaker ovvio come un segno di versione. Un attributo Column ha una proprietà IsVersion che consente di specificare se il valore dei dati costituisce un valore di versione gestito dal database. Se esiste una versione, la versione viene usata da sola per determinare se si è verificato un conflitto.

Quando si verifica un conflitto di modifiche, verrà generata un'eccezione esattamente come se fosse un altro errore. La transazione che circonda l'invio interromperà, ma DataContext rimarrà invariata, consentendo di correggere il problema e riprovare.

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

Se si apportano modifiche a un livello intermedio o a un server, la cosa più semplice che è possibile eseguire per correggere un conflitto di modifiche consiste semplicemente nell'avviare e riprovare, ricreare il contesto e riapplicare le modifiche. Altre opzioni sono descritte nella sezione seguente.

Transazioni

Una transazione è un servizio fornito dai database o da qualsiasi altro resource manager che può essere usato per garantire che si verifichino automaticamente una serie di singole azioni; significa che tutti hanno successo o tutti non lo fanno. Se non lo fanno, sono anche tutti automaticamente annullati prima che qualsiasi altro sia consentito. Se non è già presente alcuna transazione, DataContext avvia automaticamente una transazione di database per proteggere gli aggiornamenti quando si chiama SubmitChanges().

È possibile scegliere di controllare il tipo di transazione usata, il relativo livello di isolamento o quello che include effettivamente avviandolo autonomamente. L'isolamento delle transazioni usato da DataContext è noto come 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

Nell'esempio precedente viene avviata una transazione completamente serializzata creando un nuovo oggetto ambito transazione. Tutti i comandi di database eseguiti nell'ambito della transazione verranno sorvegliati dalla transazione.

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

Questa versione modificata dello stesso esempio usa il metodo ExecuteCommand() nel DataContext per eseguire una stored procedure nel database direttamente prima dell'invio delle modifiche. Indipendentemente dalle operazioni eseguite dalla stored procedure nel database, è possibile che le azioni siano parte della stessa transazione.

Se la transazione viene completata correttamente, DataContext genera tutte le informazioni di rilevamento accumulate e considera i nuovi stati delle entità come invariati. Non esegue tuttavia il rollback delle modifiche apportate agli oggetti se la transazione ha esito negativo. Ciò consente la massima flessibilità nella gestione dei problemi durante l'invio di modifiche.

È anche possibile usare una transazione SQL locale anziché il nuovo TransactionScope. LINQ to SQL offre questa funzionalità per integrare LINQ to SQL funzionalità in applicazioni ADO.NET preesistenti. Tuttavia, se si va in questo percorso, sarà necessario essere responsabili di molto più.

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

Come si può notare, l'uso di una transazione di database controllata manualmente è un po'più coinvolto. Non solo è necessario avviarlo autonomamente, è necessario indicare in modo esplicito a DataContext di usarlo assegnandolo alla proprietà Transaction . È quindi necessario usare un blocco try-catch per inserire la logica di invio, ricordare di indicare in modo esplicito alla transazione di eseguire il commit e di indicare in modo esplicito a DataContext di accettare modifiche o di interrompere le transazioni se si verifica un errore in qualsiasi momento. Inoltre, non dimenticare di impostare nuovamente la proprietà Transaction su Null al termine.

Stored procedure

Quando SubmitChanges() viene chiamato, LINQ to SQL genera ed esegue comandi SQL per inserire, aggiornare ed eliminare righe nel database. Queste azioni possono essere sostituite dagli sviluppatori di applicazioni e nel loro posto codice personalizzato possono essere usate per eseguire le azioni desiderate. In questo modo, le strutture alternative come le stored procedure di database possono essere richiamate automaticamente dal processore di modifiche.

Prendere in considerazione una stored procedure per aggiornare le unità in magazzino per la tabella Products nel database di esempio Northwind. La dichiarazione SQL della procedura è la seguente.

SQL

create proc UpdateProductStock
   @id               int,
   @originalUnits    int,
   @decrement         int
as

È possibile usare la stored procedure anziché il normale comando di aggiornamento generato automaticamente definendo un metodo nel datacontext fortemente tipizzato. Anche se la classe DataContext viene generata automaticamente dallo strumento di generazione di codice LINQ to SQL, è comunque possibile specificare questi metodi in una classe parziale di propria proprietà.

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 firma del metodo e il parametro generico indica a DataContext di usare questo metodo al posto di un'istruzione di aggiornamento generata. I parametri originali e correnti vengono usati da LINQ to SQL per passare le copie originali e correnti dell'oggetto del tipo specificato. I due parametri sono disponibili per il rilevamento dei conflitti di concorrenza ottimistica.

Nota Se si esegue l'override della logica di aggiornamento predefinita, il rilevamento dei conflitti è responsabile.

La stored procedure UpdateProductStock viene richiamata usando il metodo ExecuteCommand() del dataContext. Restituisce il numero di righe interessate e ha la firma seguente:

C#

public int ExecuteCommand(string command, params object[] parameters);

Visual Basic

Public Function ExecuteCommand(command As String, _
         ParamArray parameters() As Object) As Integer

La matrice di oggetti viene usata per passare i parametri necessari per l'esecuzione del comando.

Analogamente al metodo di aggiornamento, è possibile specificare metodi di inserimento ed eliminazione. Inserire ed eliminare metodi accettano un solo parametro del tipo di entità da aggiornare. Ad esempio, i metodi per inserire ed eliminare un'istanza di Product possono essere specificati come indicato di seguito:

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)  ... 

Classi di entità In-Depth

Uso degli attributi

Una classe di entità è simile a qualsiasi classe oggetto normale che è possibile definire come parte dell'applicazione, ad eccezione del fatto che viene annotata con informazioni speciali associate a una determinata tabella di database. Queste annotazioni vengono effettuate come attributi personalizzati nella dichiarazione della classe. Gli attributi sono significativi solo quando si usa la classe insieme a LINQ to SQL. Sono simili agli attributi di serializzazione XML in .NET Framework. Questi attributi "dati" forniscono LINQ to SQL con informazioni sufficienti per tradurre le query per gli oggetti in query SQL sul database e le modifiche apportate agli oggetti in comandi di inserimento, aggiornamento ed eliminazione sql.

È anche possibile rappresentare le informazioni di mapping usando un file di mapping XML anziché gli attributi. Questo scenario è descritto in modo più dettagliato nella sezione Mapping esterno.

Attributo Database

L'attributo Database viene usato per specificare il nome predefinito del database se non viene fornito dalla connessione. Gli attributi del database possono essere applicati a dichiarazioni DataContext fortemente tipizzata. Questo attributo è facoltativo.

Attributo Database

Proprietà Type Descrizione
Nome string Specifica il nome del database. Le informazioni vengono usate solo se la connessione stessa non specifica il nome del database. Se l'attributo database non esiste nella dichiarazione di contesto e uno non è specificato dalla connessione, si presuppone che il database abbia lo stesso nome della classe di contesto.

C#

[Database(Name="Database#5")]
public class Database5 : DataContext {
   ...
}

Visual Basic

<Database(Name:="Database#5")> _
Public Class Database5 
               Inherits DataContext
   ...
End Class

Attributo Table

L'attributo Table viene usato per designare una classe come classe di entità associata a una tabella di database. Le classi con l'attributo Table verranno trattate appositamente da LINQ to SQL.

Attributo Table

Proprietà Type Descrizione
Nome string Specifica il nome della tabella. Se queste informazioni non vengono specificate, si presuppone che la tabella abbia lo stesso nome della classe di entità.

C#

[Table(Name="Customers")]
public class Customer {
   ...
}

Visual Basic

<Table(Name:="Customers")> _
Public Class Customer 
   ...
End Class

Attributo della colonna

L'attributo Column viene usato per designare un membro di una classe di entità che rappresenta una colonna in una tabella di database. Può essere applicato a qualsiasi campo o proprietà, pubblica, privata o interna. Solo i membri identificati come colonne vengono mantenuti quando LINQ to SQL salva le modifiche al database.

Attributo Column

Proprietà Type Descrizione
Nome string Nome della colonna nella tabella o nella vista. Se non è specificata la colonna, si presuppone che abbia lo stesso nome del membro della classe.
Archiviazione string Nome dell'archiviazione sottostante. Se specificato indica LINQ to SQL come ignorare la funzione di accesso alla proprietà pubblica per il membro dati e interagire con il valore non elaborato stesso. Se non specificato LINQ to SQL ottiene e imposta il valore usando la funzione di accesso pubblica.
Dbtype string Tipo di colonna di database specificato usando tipi di database e modificatori. Questo sarà il testo esatto usato per definire la colonna in un comando di dichiarazione di tabella T-SQL. Se non specificato il tipo di colonna del database viene dedotto dal tipo di membro. Il tipo di database specifico è necessario solo se il metodo CreateDatabase() deve essere usato per creare un'istanza del database.
IsPrimaryKey Bool Se impostato su true, il membro della classe rappresenta una colonna che fa parte della chiave primaria della tabella. Se più membri della classe sono designati come ID, la chiave primaria viene detta composita delle colonne associate.
IsDbGenerated Boolean Identifica che il valore della colonna del membro viene generato automaticamente dal database. Le chiavi primarie designate IsDbGenerated=true devono avere anche un DBType con il modificatore IDENTITY . IsDbGenerated i membri vengono sincronizzati immediatamente dopo l'inserimento della riga di dati e sono disponibili dopo il completamento di SubmitChanges().
IsVersion Boolean Identifica il tipo di colonna del membro come timestamp del database o un numero di versione. I numeri di versione vengono incrementati e le colonne timestamp vengono aggiornate dal database ogni volta che la riga associata viene aggiornata. I membri con IsVersion=true vengono sincronizzati immediatamente dopo l'aggiornamento della riga di dati. I nuovi valori sono visibili dopo il completamento di SubmitChanges().
UpdateCheck UpdateCheck Determina come LINQ to SQL implementa il rilevamento dei conflitti di concorrenza ottimistica. Se non viene designato alcun membro come IsVersion=true , il rilevamento viene eseguito confrontando i valori dei membri originali con lo stato corrente del database. È possibile controllare quali membri LINQ to SQL usare durante il rilevamento dei conflitti assegnando a ogni membro un valore di enumerazione UpdateCheck.
  • Sempre: usare sempre questa colonna per il rilevamento dei conflitti
  • Mai: non usare mai questa colonna per il rilevamento dei conflitti
  • WhenChanged: usare questa colonna solo quando il membro è stato modificato dall'applicazione
IsDiscriminator Boolean Determina se il membro della classe contiene il valore di discriminazione per una gerarchia di ereditarietà.
Expression string Non influisce sull'operazione di LINQ to SQL, ma viene usata durante .CreateDatabase() come espressione SQL non elaborata che rappresenta l'espressione di colonna calcolata.
Canbenull Boolean Indica che il valore può contenere il valore Null. Questa operazione viene in genere derivata dal tipo CLR del membro dell'entità. Usare questo attributo per indicare che un valore stringa è rappresentato come colonna non nullable nel database.
AutoSync AutoSync Specifica se la colonna viene sincronizzata automaticamente dal valore generato dal database nei comandi di inserimento o aggiornamento. I valori validi per questo tag sono OnInsert, Always e Never.

Una classe di entità tipica userà gli attributi Column nelle proprietà pubbliche e archivierà i valori effettivi nei campi privati.

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

DbType viene specificato solo in modo che il metodo CreateDatabase() possa costruire la tabella con il tipo più preciso. In caso contrario, la conoscenza che la colonna sottostante è limitata a 15 caratteri non è usata.

I membri che rappresentano la chiave primaria di un tipo di database vengono spesso associati ai valori generati automaticamente.

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

Se si specifica DBType, assicurarsi di includere il modificatore IDENTITY . LINQ to SQL non aumenta un DBType personalizzato specificato. Tuttavia, se dbType viene lasciato non specificato LINQ to SQL dedurrà che il modificatore IDENTITY è necessario durante la creazione del database tramite il metodo CreateDatabase().

Analogamente, se la proprietà IsVersion è true, DBType deve specificare i modificatori corretti per designare un numero di versione o una colonna timestamp. Se non viene specificato alcun dbType, LINQ to SQL dedurrà i modificatori corretti.

È possibile controllare l'accesso a un membro associato a una colonna generata automaticamente, al timbro della versione o a qualsiasi colonna che si desidera nascondere designando il livello di accesso del membro o anche limitando la funzione di accesso stessa.

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 proprietà CustomerID di Order può essere resa di sola lettura non definendo una funzione di accesso set. LINQ to SQL possibile ottenere e impostare il valore sottostante tramite il membro di archiviazione.

È anche possibile rendere un membro completamente inaccessibile al resto dell'applicazione inserendo un attributo Column su un membro privato. In questo modo la classe di entità può contenere informazioni rilevanti per la logica di business della classe senza esponerla in generale. Anche se i membri privati fanno parte dei dati tradotti, poiché sono privati, non è possibile farvi riferimento in una query integrata nel linguaggio.

Per impostazione predefinita, tutti i membri vengono usati per eseguire il rilevamento dei conflitti di concorrenza ottimistica. È possibile controllare se un determinato membro viene utilizzato specificando il relativo valore 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

La tabella seguente illustra i mapping consentiti tra i tipi di database e il tipo CLR corrispondente. Utilizzare questa tabella come guida per determinare il tipo CLR da utilizzare per rappresentare una determinata colonna di database.

Mapping consentiti per il tipo di database e il tipo CLR corrispondente

Tipo di database Tipo CLR .NET Commenti
bit, tinyint, smallint, int, bigint Bye, Int16, Uint16, Int32, Uint32, Int64, Uint64 Conversioni di perdita possibili. I valori potrebbero non essere di andata e ritorno.
bit Boolean  
decimal, numeric, smallmoney, money Decimal La differenza di scala può comportare una conversione con perdita di dati. Non può essere andata e ritorno.
real, float Single, Double Differenze di precisione.
char, varchar, text, nchar, nvarchar, ntext string Differenze tra le impostazioni locali possibili.
datetime, smalldatetime Datetime Una precisione diversa può causare problemi di conversione e round trip con perdita.
UNIQUEIDENTIFIER Guid Regole di confronto diverse. L'ordinamento potrebbe non funzionare come previsto.
timestamp Byte[] (Byte() in Visual Basic), Binary La matrice di byte viene considerata come un tipo scalare. L'utente è responsabile dell'allocazione di spazio di archiviazione adeguato quando viene chiamato il costruttore. È considerato non modificabile e non viene monitorato per le modifiche.
binary, varbinary Byte[] (Byte() in Visual Basic), Binary  

Attributo association

L'attributo Association viene utilizzato per designare una proprietà che rappresenta un'associazione di database come una relazione di chiave esterna a chiave primaria.

Attributo association

Proprietà Type Descrizione
Nome string Nome dell'associazione. Si tratta spesso dello stesso nome del vincolo di chiave esterna del database. Viene usato quando viene usato CreateDatabase() per creare un'istanza del database per generare il vincolo pertinente. Viene usato anche per distinguere tra più relazioni in una singola classe di entità che fa riferimento alla stessa classe di entità di destinazione. In questo caso, le proprietà della relazione sui lati della relazione (se entrambe sono definite) devono avere lo stesso nome.
Archiviazione string Nome del membro di archiviazione sottostante. Se specificato, indica LINQ to SQL come ignorare la funzione di accesso della proprietà pubblica per il membro dati e interagire con il valore non elaborato stesso. Se non specificato LINQ to SQL ottiene e imposta il valore usando la funzione di accesso pubblica. È consigliabile che tutti i membri dell'associazione siano proprietà con membri di archiviazione separati identificati.
ThisKey string Elenco delimitato da virgole di uno o più membri di questa classe di entità che rappresentano i valori chiave su questo lato dell'associazione. Se non specificato, si presuppone che i membri siano i membri che costituiscono la chiave primaria.
OtherKey string Elenco delimitato da virgole di uno o più membri della classe di entità di destinazione che rappresentano i valori chiave sull'altro lato dell'associazione. Se non specificato, si presuppone che i membri siano i membri che costituiscono la chiave primaria dell'altra classe di entità.
IsUnique Boolean True se nella chiave esterna è presente un vincolo di univocità, che indica una relazione true 1:1. Questa proprietà viene utilizzata raramente come relazioni 1:1 quasi impossibile da gestire all'interno del database. Per lo più i modelli di entità vengono definiti usando relazioni 1:n anche quando vengono considerati come 1:1 dagli sviluppatori di applicazioni.
IsForeignKey Boolean True se il tipo di destinazione "other" dell'associazione è l'elemento padre del tipo di origine. Con le relazioni tra chiave esterna e chiave primaria, il lato che contiene la chiave esterna è l'elemento figlio e il lato che contiene la chiave primaria è l'elemento padre.
DeleteRule string Usato per aggiungere il comportamento di eliminazione a questa associazione. Ad esempio, "CASCADE" aggiunge "ON DELETE CASCADE" alla relazione FK. Se impostato su Null, non viene aggiunto alcun comportamento di eliminazione.

Le proprietà di associazione rappresentano un singolo riferimento a un'altra istanza della classe di entità oppure rappresentano una raccolta di riferimenti. I riferimenti Singleton devono essere codificati nella classe di entità usando il tipo di valore EntityRef T> (EntityRef<(OfT) in Visual Basic per archiviare il riferimento effettivo. Il tipo EntityRef è il modo in cui LINQ to SQL abilita il caricamento posticipato dei riferimenti.

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 proprietà pubblica viene tipizzata come Customer, non EntityRef<Customer>. È importante non esporre il tipo EntityRef come parte dell'API pubblica, poiché i riferimenti a questo tipo in una query non verranno convertiti in SQL.

Analogamente, una proprietà di associazione che rappresenta un insieme deve utilizzare il tipo di raccolta EntitySet T> (EntitySet<(OfT) in Visual Basic) per archiviare la relazione.

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

Tuttavia, poiché EntitySet T> (EntitySet(OfT) in Visual Basic è una raccolta, è valido usare EntitySet< come tipo restituito. È anche valido mascherare il tipo vero della raccolta, usando invece l'interfaccia ICollection<T> (ICollection(OfT) in 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

Assicurarsi di usare il metodo Assign() in EntitySet se si espone un setter pubblico per la proprietà . Ciò consente alla classe di entità di continuare a usare la stessa istanza di raccolta perché potrebbe essere già associata al servizio di rilevamento delle modifiche.

Attributo ResultType

Questo attributo specifica un tipo di elemento di una sequenza enumerabile che può essere restituita da una funzione dichiarata per restituire l'interfaccia IMultipleResults . Questo attributo può essere specificato più volte.

Attributo ResultType

Proprietà Type Descrizione
Tipo Tipo Tipo dei risultati restituiti.

Attributo StoredProcedure

L'attributo StoredProcedure viene utilizzato per dichiarare che una chiamata a un metodo definito nel tipo DataContext o Schema viene convertita come chiamata a una stored procedure di database.

Attributo StoredProcedure

Proprietà Type Descrizione
Nome string Nome della stored procedure nel database. Se non viene specificata, si presuppone che la stored procedure abbia lo stesso nome del metodo

Attributo function

L'attributo Function viene usato per dichiarare che una chiamata a un metodo definito in un oggetto DataContext o Schema viene convertita come chiamata a una funzione scalare o con valori di tabella definita dall'utente del database.

Attributo function

Proprietà Type Descrizione
Nome string Nome della funzione nel database. Se non viene specificata, si presuppone che la funzione abbia lo stesso nome del metodo

Attributo

L'attributo Parameter viene usato per dichiarare un mapping tra un metodo e i parametri di una stored procedure del database o di una funzione definita dall'utente.

Attributo

Proprietà Type Descrizione
Nome string Nome del parametro nel database. Se non specificato, il parametro viene dedotto dal nome del parametro del metodo.
Dbtype string Tipo di parametro specificato utilizzando tipi di database e modificatori.

Attributo InheritanceMapping

L'attributo InheritanceMapping viene utilizzato per descrivere la corrispondenza tra un determinato codice discriminatorio e un sottotipo di ereditarietà. Tutti gli attributi InheritanceMapping usati per una gerarchia di ereditarietà devono essere dichiarati nel tipo radice della gerarchia.

Attributo InheritanceMapping

Propety Tipo Descrizione
Codice Oggetto Valore del codice discriminatorio.
Tipo Tipo Sottotipo Ereditarietà. Può trattarsi di qualsiasi tipo non astratto nella gerarchia di ereditarietà, incluso il tipo radice.
IsDefault Boolean Determina se il sottotipo di ereditarietà specificato è il tipo predefinito costruito quando LINQ to SQL trova un codice discriminatorio non definito dagli attributi InheritanceMapping. È necessario dichiarare esattamente uno degli attributi InheritanceMapping con IsDefault come true.

Coerenza del grafo

Un grafico è un termine generale per una struttura di dati di oggetti che fanno riferimento l'uno all'altro in base ai riferimenti. Una gerarchia (o albero) è una forma degenerata di grafo. I modelli a oggetti specifici del dominio spesso descrivono una rete di riferimenti che sono meglio descritti come grafico di oggetti. L'integrità dell'oggetto grafico è fondamentale per la stabilità dell'applicazione. Ecco perché è importante assicurarsi che i riferimenti all'interno del grafico rimangano coerenti con le regole business e/o i vincoli definiti nel database.

LINQ to SQL non gestisce automaticamente la coerenza dei riferimenti alle relazioni. Quando le relazioni sono bidirezionali, una modifica a un lato della relazione deve aggiornare automaticamente l'altra. Si noti che è insolito che gli oggetti normali si comportino in questo modo, quindi è improbabile che gli oggetti siano stati progettati in questo modo in caso contrario.

LINQ to SQL fornisce alcuni meccanismi per semplificare questo lavoro e un modello da seguire per assicurarsi di gestire correttamente i riferimenti. Le classi di entità generate dallo strumento di generazione del codice implementeranno automaticamente i modelli corretti.

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
);}

Il tipo EntitySet<T> (EntitySet(OfT) in Visual Basic include un costruttore che consente di fornire due delegati da utilizzare come callback. Il primo quando un elemento viene aggiunto alla raccolta, il secondo quando viene rimosso. Come si può notare nell'esempio, il codice specificato per questi delegati può e deve essere scritto per aggiornare la proprietà della relazione inversa. Questo è il modo in cui la proprietà Customer in un'istanza order viene modificata automaticamente quando un ordine viene aggiunto alla raccolta Orders di un cliente.

L'implementazione della relazione sull'altra estremità non è altrettanto semplice. EntityRef<T> (EntityRef(OfT) in Visual Basic è un tipo di valore definito per contenere il minor sovraccarico aggiuntivo possibile dal riferimento effettivo all'oggetto. Non ha spazio per un paio di delegati. Il codice che gestisce invece la coerenza del grafico dei riferimenti singleton deve essere incorporato nelle funzioni di accesso della proprietà.

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

Guarda il setter. Quando la proprietà Customer viene modificata, l'istanza dell'ordine viene prima rimossa dalla raccolta Orders del cliente corrente e quindi aggiunta solo successivamente alla raccolta del nuovo cliente. Si noti che prima che venga eseguita la chiamata a Remove() il riferimento effettivo all'entità è impostato su Null. Questa operazione viene eseguita per evitare la ricorsione quando viene chiamato il metodo Remove(). Tenere presente che EntitySet userà delegati di callback per assegnare la proprietà Customer dell'oggetto a null. La stessa cosa accade subito prima della chiamata a Add(). Il riferimento effettivo all'entità viene aggiornato al nuovo valore. Ciò consentirà di ridurre nuovamente qualsiasi potenziale ricorsione e naturalmente eseguire l'attività del setter al primo posto.

La definizione di una relazione uno-a-uno è molto simile alla definizione di una relazione uno-a-molti dal lato del riferimento singleton. Anziché chiamare Add()e Remove(), viene assegnato un nuovo oggetto o un valore Null viene assegnato a una relazione.

Di nuovo, è fondamentale che le proprietà della relazione mantengano la coerenza del grafico a oggetti. Se il grafico a oggetti in memoria non è coerente con i dati del database, viene generata un'eccezione di runtime quando viene chiamato il metodo SubmitChanges . È consigliabile usare lo strumento di generazione del codice per mantenere il lavoro di coerenza.

Notifiche di modifica

Gli oggetti possono partecipare al processo di rilevamento delle modifiche. Non è necessario che lo facciano, ma possono ridurre notevolmente la quantità di sovraccarico necessaria per tenere traccia delle potenziali modifiche degli oggetti. È probabile che l'applicazione recupera molti più oggetti dalle query che finiranno per modificare. Senza assistenza proattiva dagli oggetti, il servizio di rilevamento delle modifiche è limitato in modo che possa effettivamente tenere traccia delle modifiche.

Poiché non esiste un vero servizio di intercettazione nel runtime, il rilevamento formale non si verifica effettivamente. Al contrario, le copie duplicate degli oggetti vengono archiviate quando vengono recuperate per la prima volta. In seguito, quando si chiama SubmitChanges(), queste copie vengono usate per confrontare con quelle fornite. Se i valori differiscono, l'oggetto è stato modificato. Ciò significa che ogni oggetto richiede due copie in memoria anche se non le si modifica mai.

Una soluzione migliore consiste nell'annunciare gli oggetti stessi al servizio rilevamento modifiche quando vengono effettivamente modificati. Questa operazione può essere eseguita tramite l'implementazione dell'oggetto di un'interfaccia che espone un evento di callback. Il servizio di rilevamento delle modifiche può quindi collegare ogni oggetto e ricevere notifiche quando cambiano.

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

Per facilitare il rilevamento delle modifiche, le classi di entità devono implementare l'interfaccia INotifyPropertyChanging . Richiede solo di definire un evento denominato PropertyChanging, ovvero il servizio di rilevamento delle modifiche e quindi registra l'evento quando gli oggetti vengono inseriti nel suo possesso. Tutto ciò che è necessario eseguire è generare questo evento immediatamente prima di modificare il valore di una proprietà.

Non dimenticare di inserire anche la stessa logica di generazione di eventi nei setter delle proprietà di relazione. Per EntitySets, generare gli eventi nei delegati forniti.

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

Ereditarietà

LINQ to SQL supporta il mapping a tabella singola, in cui viene archiviata un'intera gerarchia di ereditarietà in una singola tabella di database. La tabella contiene l'unione flat di tutte le colonne di dati possibili per l'intera gerarchia e ogni riga ha valori Null nelle colonne che non sono applicabili al tipo dell'istanza rappresentata dalla riga. La strategia di mapping di singole tabelle è la più semplice rappresentazione di ereditarietà e offre caratteristiche di prestazioni ottimali per numerose categorie di query.

Mapping

Per implementare questo mapping usando LINQ to SQL, è necessario specificare gli attributi e le proprietà dell'attributo seguenti nella classe radice della gerarchia di ereditarietà:

  • Attributo [Table] (<Table> in Visual Basic).
  • Attributo [EreditarietàMapping] (<EreditarietàMapping> in Visual Basic) per ogni classe nella struttura della gerarchia. Per le classi non astratte, questo attributo deve definire una proprietà Code (un valore visualizzato nella tabella di database nella colonna Discriminazione ereditarietà per indicare quale classe o sottoclasse appartiene a questa riga di dati) e una proprietà Type (che specifica quale classe o sottoclasse indica il valore della chiave).
  • Proprietà IsDefault in un singolo attributo [EreditarietàMapping] (<EreditarietàMapping> in Visual Basic). Questa proprietà serve per designare un mapping "fallback" nel caso in cui il valore di discriminazione dalla tabella di database non corrisponda a nessuno dei valori Di codice nei mapping di ereditarietà.
  • Proprietà IsDiscriminator per un attributo [Column] (<Column> in Visual Basic) per firmare che si tratta della colonna che contiene il valore Code per il mapping di ereditarietà.

Non sono richiesti attributi o proprietà speciali sulle sottoclassi. Si noti soprattutto che le sottoclassi non hanno l'attributo [Table] (<Table> in Visual Basic).

Nell'esempio seguente i dati contenuti nelle sottoclassi Car and Truck vengono mappati alla tabella di database singola Veicolo. Per semplificare l'esempio, il codice di esempio usa campi anziché proprietà per il mapping delle colonne.

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

Il diagramma delle classi viene visualizzato come segue:

Figura 1. Diagramma della classe del veicolo

Quando si visualizza il diagramma del database risultante in Esplora server, si noterà che tutte le colonne sono state mappate a una singola tabella, come illustrato di seguito:

Figura 2. Colonne mappate a una singola tabella

Si noti che i tipi delle colonne che rappresentano i campi nei sottotipi devono essere nullable oppure devono avere un valore predefinito specificato. È necessario che i comandi di inserimento siano riusciti.

Query

Il codice seguente offre un sapore di come è possibile usare tipi derivati nelle query:

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

Avanzato

È possibile espandere una gerarchia molto oltre l'esempio semplice già fornito.

Esempio 1

Ecco una gerarchia molto più profonda e una query più complessa:

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)

Esempio 2

La gerarchia seguente include interfacce:

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

Le query possibili includono quanto segue:

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))

Argomenti avanzati

Creazione di database

Poiché le classi di entità hanno attributi che descrivono la struttura delle tabelle e delle colonne del database relazionale, è possibile usare queste informazioni per creare nuove istanze del database. È possibile chiamare il metodo CreateDatabase() in DataContext per avere LINQ to SQL costruire una nuova istanza di database con una struttura definita dagli oggetti. Esistono molti motivi per cui si vuole eseguire questa operazione: è possibile creare un'applicazione che si installa automaticamente in un sistema cliente o in un'applicazione client che necessita di un database locale per salvare lo stato offline. Per questi scenari, CreateDatabase() è ideale, soprattutto se è disponibile un provider di dati noto come SQL Server Express 2005.

Tuttavia, gli attributi dei dati potrebbero non codificare tutti gli elementi relativi a una struttura di database esistente. Il contenuto di funzioni definite dall'utente, stored procedure, trigger e vincoli di controllo non sono rappresentati dagli attributi. La funzione CreateDatabase() creerà solo una replica del database usando le informazioni che conosce, ovvero la struttura del database e i tipi di colonne in ogni tabella. Tuttavia, per un'ampia gamma di database è sufficiente.

Di seguito è riportato un esempio di come creare un nuovo database denominato 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

Il modello a oggetti può essere usato per la creazione di un database usando SQL Server Express 2005 come indicato di seguito:

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 fornisce anche un'API per eliminare un database esistente prima di crearne uno nuovo. Il codice di creazione del database precedente può essere modificato per verificare prima di tutto una versione esistente del database usando DatabaseExists() e quindi eliminarla usando 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()

Dopo la chiamata a CreateDatabase(), il nuovo database è in grado di accettare query e comandi come SubmitChanges() per aggiungere oggetti al file MDF.

È anche possibile usare CreateDatabase() con uno SKU diverso da SQL Server Express, usando un file MDF o solo un nome di catalogo. Tutto dipende da ciò che si usa per la stringa di connessione. Le informazioni nella stringa di connessione vengono utilizzate per definire il database che esisterà, non necessariamente quello già esistente. LINQ to SQL pescare i bit di informazioni pertinenti e usarli per determinare quale database creare e in quale server crearlo. A tale scopo, naturalmente, saranno necessari diritti di amministratore del database o equivalenti nel server.

Interoperabilità con ADO.NET

LINQ to SQL fa parte della famiglia di tecnologie ADO.NET. Si basa sui servizi forniti dal modello di provider ADO.NET, quindi è possibile combinare LINQ to SQL codice con le applicazioni di ADO.NET esistenti.

Quando si crea un LINQ to SQL DataContext, è possibile specificarlo con una connessione ADO.NET esistente. Tutte le operazioni su DataContext, incluse le query, useranno la connessione fornita. Se la connessione è già stata aperta LINQ to SQL rispetterà l'autorità sulla connessione e la lascerà invariata al termine. In genere LINQ to SQL chiude la connessione non appena viene completata un'operazione, a meno che una transazione non sia nell'ambito.

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

È sempre possibile accedere alla connessione utilizzata da DataContext tramite la proprietà Connection e chiuderla manualmente.

C#

db.Connection.Close(); 

Visual Basic

db.Connection.Close()

È anche possibile specificare DataContext con la propria transazione di database, nel caso in cui l'applicazione ne abbia già avviata una e si desideri che DataContext venga riprodotta insieme.

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

Ogni volta che viene impostata una transazione , DataContext lo userà ogni volta che emette una query o esegue un comando. Al termine, non dimenticare di assegnare la proprietà a null .

Tuttavia, il metodo preferito per eseguire transazioni con .NET Framework consiste nell'utilizzare l'oggetto TransactionScope . Consente di eseguire transazioni distribuite che funzionano tra database e altri gestori di risorse residenti in memoria. L'idea è che gli ambiti delle transazioni iniziano a buon mercato, promuovendo se stessi a pieno nella transazione distribuita quando effettivamente fanno riferimento a più database o a più connessioni all'interno dell'ambito della transazione.

C#

using(TransactionScope ts = new TransactionScope()) {
   db.SubmitChanges();
   ts.Complete();
}

Visual Basic

Using ts As TransactionScope= New TransactionScope()
   db.SubmitChanges()
   ts.Complete()
End Using

Esecuzione diretta di istruzioni SQL

Le connessioni e le transazioni non sono l'unico modo per interagire con ADO.NET. È possibile che in alcuni casi la query o la funzionalità di invio delle modifiche di DataContext non sia sufficiente per l'attività specializzata che si desidera eseguire. In questi casi è possibile usare DataContext per eseguire comandi SQL non elaborati direttamente nel database.

Il metodo ExecuteQuery() consente di eseguire una query SQL non elaborata e di convertire il risultato della query direttamente in oggetti. Ad esempio, supponendo che i dati per la classe Customer siano distribuiti su due tabelle customer1 e customer2, la query seguente restituisce una sequenza di oggetti 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" )

Se i nomi delle colonne nei risultati tabulari corrispondono alle proprietà della colonna della classe di entità LINQ to SQL materializzeranno gli oggetti da qualsiasi query SQL.

Il metodo ExecuteQuery() consente anche i parametri. Nel codice seguente viene eseguita una query con parametri:

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" )

I parametri vengono espressi nel testo della query usando la stessa notazione curly usata da Console.WriteLine() e String.Format(). In realtà , String.Format() viene effettivamente chiamato sulla stringa di query specificata, sostituendo i parametri con parentesi graffe con nomi di parametri generati come p0, @p1 ..., p(n).

Modifica risoluzione dei conflitti

Descrizione

Un conflitto di modifiche si verifica quando il client tenta di inviare modifiche a un oggetto e uno o più valori utilizzati nel controllo degli aggiornamenti sono stati aggiornati nel database dall'ultima lettura del client.

Nota Solo i membri mappati come UpdateCheck.Always o UpdateCheck.WhenChanged partecipano ai controlli di concorrenza ottimistica. Non viene eseguito alcun controllo per i membri contrassegnati come UpdateCheck.Never.

La risoluzione di questo conflitto include l'individuazione dei membri dell'oggetto in conflitto e quindi la decisione su cosa fare. Si noti che la concorrenza ottimistica potrebbe non essere la strategia migliore nella situazione specifica. A volte è perfettamente ragionevole "lasciare vincere l'ultimo aggiornamento".

Rilevamento, creazione di report e risoluzione dei conflitti in LINQ to SQL

La risoluzione dei conflitti è il processo di aggiornamento di un elemento in conflitto eseguendo nuovamente una query sul database e riconciliando eventuali differenze. Quando un oggetto viene aggiornato, lo strumento di rilevamento delle modifiche include i valori originali precedenti e i nuovi valori del database. LINQ to SQL quindi determina se l'oggetto è in conflitto o meno. In caso affermativo, LINQ to SQL determina quali membri sono coinvolti. Se il nuovo valore del database per un membro è diverso dal precedente originale (usato per il controllo di aggiornamento non riuscito), si tratta di un conflitto. Eventuali conflitti di membri vengono aggiunti a un elenco di conflitti.

Ad esempio, nello scenario seguente, User1 inizia a preparare un aggiornamento eseguendo una query sul database per una riga. Prima che User1 possa inviare le modifiche, User2 ha modificato il database. L'invio di User1 ha esito negativo perché i valori previsti per Col B e Col C sono stati modificati.

Conflitto di aggiornamento del database

Utente Col A Col B Col C
Stato originale Alfreds Maria Sales
Utente 1 Alfred   Marketing
Utente 2   Mary Servizio

In LINQ to SQL gli oggetti che non vengono aggiornati a causa di conflitti di concorrenza ottimistica causano la generazione di un'eccezione (ChangeConflictException). È possibile specificare se l'eccezione deve essere generata al primo errore o se tutti gli aggiornamenti devono essere tentati con eventuali errori accumulati e segnalati nell'eccezione.

// [C#]
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
db.SubmitChanges(ConflictMode.ContinueOnConflict);
' [Visual Basic]
db.SubmitChanges(ConflictMode.FailOnFirstConflict)
db.SubmitChanges(ConflictMode.ContinueOnConflict)

Quando viene generata, l'eccezione fornisce l'accesso a un insieme ObjectChangeConflict . I dettagli sono disponibili per ogni conflitto (mappato a un singolo tentativo di aggiornamento non riuscito), incluso l'accesso all'elenco MemberConflicts . Per ogni conflitto fra membri viene eseguito il mapping a un unico membro nell'aggiornamento che non ha superato il controllo della concorrenza.

Gestione dei conflitti

Nello scenario precedente, User1 include le opzioni RefreshMode descritte di seguito per riconciliare le differenze prima di tentare di inviare di nuovo. In tutti i casi, il record nel client viene prima "aggiornato" trascinando i dati aggiornati dal database. Questa azione garantisce che il tentativo di aggiornamento successivo non avrà esito negativo sugli stessi controlli di concorrenza.

In questo caso, User1 sceglie di unire i valori del database con i valori client correnti in modo che i valori del database vengano sovrascritti solo quando anche il set di modifiche corrente ha modificato tale valore. Vedere l'esempio 1 più avanti in questa sezione.

Nello scenario precedente, dopo la risoluzione dei conflitti, il risultato nel database è il seguente:

KeepChanges

  Col A Col B Col C
KeepChanges Alfred (Utente 1) Mary (Utente 2) Marketing (Utente 1)
  • Col A: viene visualizzata la modifica di User1 (Alfred).
  • Col B: viene visualizzata la modifica di User2 (Mary). Questo valore è stato unito perché User1 non lo ha modificato.
  • Col C: viene visualizzata la modifica di User1 (Marketing). La modifica di User2 (servizio) non viene unita perché User1 ha modificato anche l'elemento.

Di seguito, User1 sceglie di sovrascrivere i valori del database con i valori correnti. Vedere l'esempio 2 più avanti in questa sezione.

Dopo l'aggiornamento, vengono inviate le modifiche di User1. Il risultato nel database è il seguente:

KeepCurrentValues

  Col A Col B Col C
KeepCurrentValues Alfred (Utente 1) Maria (originale) Marketing (Utente 1)
  • Col A: viene visualizzata la modifica di User1 (Alfred).
  • Col B: L'originale Maria rimane; La modifica di User2 viene eliminata.
  • Col C: viene visualizzata la modifica di User1 (Marketing). La modifica di User2 (servizio) viene eliminata.

Nello scenario successivo, User1 sceglie di consentire ai valori del database di sovrascrivere i valori correnti nel client. Vedere l'esempio 3 più avanti in questa sezione.

Nello scenario precedente, dopo la risoluzione dei conflitti, il risultato nel database è il seguente:

SovrascriviCurrentValues

  Col A Col B Col C
SovrascriviCurrentValues Alfreds (originale) Mary (Utente 2) Servizio (Utente 2)
  • Col A: Il valore originale (Alfreds) rimane; Il valore di User1 (Alfred) viene rimosso.
  • Col B: viene visualizzata la modifica di User2 (Mary).
  • Col C: viene visualizzata la modifica di User2 (servizio). La modifica di User1 (Marketing) viene eliminata.

Dopo la risoluzione dei conflitti, è possibile tentare di inviare di nuovo. Poiché questo secondo aggiornamento potrebbe non riuscire, è consigliabile usare un ciclo per i tentativi di aggiornamento.

Esempio

Gli estratti di codice seguenti mostrano vari membri informativi e tecniche a vostra disposizione per individuare e risolvere i conflitti dei membri.

Esempio 1

In questo esempio i conflitti vengono risolti "automaticamente". Ovvero, i valori del database vengono uniti ai valori client correnti, a meno che il client non abbia modificato anche tale valore (KeepChanges). Non viene eseguita alcuna ispezione o gestione personalizzata dei conflitti dei singoli membri.

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)

Esempio 2

In questo esempio i conflitti vengono risolti di nuovo senza alcuna gestione personalizzata. Questa volta, tuttavia, i valori del database non vengono uniti nei valori client correnti.

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

Esempio 3

Anche in questo caso, non viene eseguita alcuna gestione personalizzata. In questo caso, tuttavia, tutti i valori client vengono aggiornati con i valori del database correnti.

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

Esempio 4

In questo esempio viene illustrato un modo per accedere alle informazioni su un'entità in conflitto.

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

Esempio 5

In questo esempio viene aggiunto un ciclo tra i singoli membri. Qui è possibile fornire una gestione personalizzata di qualsiasi membro.

Nota Aggiungere usando System.Reflection; per specificare 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

Chiamata di stored procedure

LINQ to SQL supporta stored procedure e funzioni definite dall'utente. LINQ to SQL esegue il mapping di queste astrazioni definite dal database agli oggetti client generati dal codice, in modo che sia possibile accedervi in modo fortemente tipizzato dal codice client. È possibile individuare facilmente questi metodi usando IntelliSense e le firme dei metodi sono simili al più possibile le firme delle procedure e delle funzioni definite nel database. Un set di risultati restituito da una chiamata a una routine mappata è una raccolta fortemente tipizzata. LINQ to SQL può generare automaticamente i metodi mappati, ma supporta anche il mapping manuale nelle situazioni in cui si sceglie di non usare la generazione di codice.

LINQ to SQL esegue il mapping di stored procedure e funzioni ai metodi tramite l'uso di attributi. Gli attributi StoredProcedure, Parameter e Function supportano tutti una proprietà Name e l'attributo Parameter supporta anche una proprietà DBType . Di seguito sono riportati due esempi:

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

Negli esempi seguenti vengono illustrati i mapping per vari tipi di stored procedure.

Esempio 1

La stored procedure seguente accetta un singolo parametro di input e restituisce un numero intero:

CREATE PROCEDURE GetCustomerOrderCount(@CustomerID nchar(5))
AS
Declare @count int
SELECT @count = COUNT(*) FROM ORDERS WHERE CustomerID = @CustomerID
RETURN @count

Il metodo mappato sarà il seguente:

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

Esempio 2

Quando una stored procedure consente di restituire più forme di risultati, il tipo restituito non può essere fortemente tipizzato a una singola forma di proiezione. Nell'esempio seguente la forma del risultato dipende dall'input:

CREATE PROCEDURE VariableResultShapes(@shape int)
AS
if(@shape = 1)
   select CustomerID, ContactTitle, CompanyName from customers
else if(@shape = 2)
   select OrderID, ShipName from orders

Il metodo mappato è il seguente:

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

È possibile usare questa stored procedure come indicato di seguito:

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 
         
      }           

Qui è necessario usare il modello GetResult per ottenere un enumeratore del tipo corretto, in base alla conoscenza della stored procedure. LINQ to SQL può generare tutti i tipi di proiezione possibili, ma non ha modo di sapere in quale ordine verranno restituiti. L'unico modo per sapere quali tipi di proiezione generati corrispondono a un metodo mappato consiste nell'usare commenti di codice generati sui metodi.

Esempio 3

Di seguito è riportato il codice T-SQL di una stored procedure che restituisce più forme risultato in sequenza:

CREATE PROCEDURE MultipleResultTypesSequentially
AS
select * from products
select * from customers

LINQ to SQL eseguirebbe il mapping di questa procedura esattamente come nell'esempio 2 precedente. In questo caso, tuttavia, sono presenti due set di risultati sequenziali .

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

È possibile usare questa stored procedure come indicato di seguito:

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

Esempio 4

LINQ to SQL esegue il mapping out dei parametri ai parametri di riferimento (parola chiave ref) e per i tipi di valore dichiara il parametro come nullable (ad esempio, int?). La procedura nell'esempio seguente accetta un singolo parametro di input e restituisce un out parametro.

CREATE PROCEDURE GetCustomerCompanyName(
   @customerID nchar(5),
   @companyName nvarchar(40) output
   )
AS
SELECT @companyName = CompanyName FROM Customers
WHERE CustomerID=@CustomerID

Il metodo mappato è il seguente:

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

In questo caso, il metodo non ha un valore restituito esplicito, ma il valore restituito predefinito viene mappato comunque. Per il parametro di output, viene usato un parametro di output corrispondente come previsto.

Si chiamerebbe la stored procedure precedente come indicato di seguito:

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)

Funzioni definite dall'utente

LINQ to SQL supporta entrambe le funzioni scalari e con valori di tabella e supporta la controparte in linea di entrambi.

LINQ to SQL gestisce chiamate scalari inline in modo analogo al modo in cui vengono chiamate le funzioni definite dal sistema. Si consideri la query seguente:

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)

In questo caso il metodo chiama Math.Floor viene convertito in una chiamata alla funzione di sistema 'FLOOR'. Allo stesso modo, una chiamata a una funzione mappata a una funzione definita dall'utente viene tradotta in una chiamata alla funzione definita dall'utente in SQL.

Esempio 1

Ecco una funzione definita dall'utente scalare (UDF) ReverseCustName(). In SQL Server la funzione potrebbe essere definita come segue:

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

È possibile eseguire il mapping di un metodo client definito in una classe schema a questa funzione UDF usando il codice seguente. Si noti che il corpo del metodo costruisce un'espressione che acquisisce la finalità della chiamata al metodo e passa tale espressione al DataContext per la traduzione e l'esecuzione. Questa esecuzione diretta si verifica solo se la funzione viene chiamata.

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)

Esempio 2

Nella query seguente è possibile visualizzare una chiamata inline al metodo UDF generato ReverseCustName. In questo caso la funzione non viene eseguita immediatamente. Sql compilato per questa query si traduce in una chiamata alla funzione definita nel database (vedere il codice SQL che segue la query).

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]

Quando si chiama la stessa funzione all'esterno di una query, LINQ to SQL crea una semplice query dall'espressione di chiamata al metodo con la sintassi SQL seguente (dove il parametro @p0 è associato alla costante passata):

In LINQ to SQL:

C#

string str = db.ReverseCustName("LINQ to SQL");

Visual Basic

Dim str As String = db.ReverseCustName("LINQ to SQL")

Converte in:

SELECT dbo.ReverseCustName(@p0)

Esempio 3

Una funzione con valori di tabella (TVF) restituisce un singolo set di risultati (a differenza delle stored procedure, che può restituire più forme di risultato). Poiché il tipo restituito TVF è tabella, è possibile usare una tabella TVF ovunque in SQL che è possibile usare una tabella e è possibile trattare la tvF allo stesso modo di una tabella.

Prendere in considerazione la definizione di SQL Server seguente di una funzione con valori di tabella:

CREATE FUNCTION ProductsCostingMoreThan(@cost money)
RETURNS TABLE
AS
RETURN
   SELECT ProductID, UnitPrice
   FROM Products
   WHERE UnitPrice > @cost

Questa funzione indica in modo esplicito che restituisce una TABELLA, quindi la struttura del set di risultati restituita viene definita in modo implicito. LINQ to SQL esegue il mapping della funzione come indicato di seguito:

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)

Il codice SQL seguente mostra che è possibile aggiungere alla tabella restituita dalla funzione e in caso contrario considerarlo come si farebbe con qualsiasi altra tabella:

SELECT p2.ProductName, p1.UnitPrice
FROM dbo.ProductsCostingMoreThan(80.50)
AS p1 INNER JOIN Products AS p2 ON p1.ProductID = p2.ProductID

In LINQ to SQL la query verrà eseguito il rendering come indicato di seguito (usando la nuova sintassi "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

limitazioni LINQ to SQL sulle stored procedure

LINQ to SQL supporta la generazione di codice per stored procedure che restituiscono set di risultati determinati in modo statico. Pertanto, il generatore di codice LINQ to SQL non supporta quanto segue:

  • Stored procedure che usano SQL dinamico per restituire i set di risultati. Quando una stored procedure contiene la logica condizionale per compilare un'istruzione SQL dinamica, LINQ to SQL non è in grado di acquisire metadati per il set di risultati perché la query usata per generare il set di risultati non è sconosciuta fino al momento dell'esecuzione.
  • Stored procedure che producono risultati in base alla tabella temporanea.

Strumento generatore di classi di entità

Se si dispone di un database esistente, non è necessario creare un modello a oggetti completo solo per rappresentarlo. La distribuzione LINQ to SQL include uno strumento denominato SQLMetal. È un'utilità della riga di comando che automatizza l'attività di creazione di classi di entità inferendo le classi appropriate dai metadati del database.

È possibile usare SQLMetal per estrarre i metadati SQL da un database e generare un file di origine contenente dichiarazioni di classe di entità. In alternativa, è possibile suddividere il processo in due passaggi, generando prima un file XML che rappresenta i metadati SQL e quindi traducendo il file XML in un file di origine contenente dichiarazioni di classe. Questo processo di suddivisione consente di conservare i metadati come file in modo da poterlo modificare. Il processo di estrazione che produce il file effettua alcune inferenze lungo la strada per i nomi di classe e proprietà appropriati in base ai nomi di tabella e colonna del database. Potrebbe essere necessario modificare il file XML per consentire al generatore di produrre risultati più piacevoli o nascondere gli aspetti del database che non si desidera presentare negli oggetti.

Lo scenario più semplice da usare SQLMetal consiste nel generare direttamente classi da un database esistente. Ecco come richiamare lo strumento:

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'esecuzione dello strumento crea un file Northwind.cs o vb contenente il modello a oggetti generato leggendo i metadati del database. Questo utilizzo funziona correttamente se i nomi delle tabelle nel database sono simili ai nomi degli oggetti che si desidera generare. Se non si vuole adottare l'approccio in due passaggi.

Per indicare a SQLMetal di generare un file DBML, usare lo strumento come indicato di seguito:

SqlMetal /server:.\SQLExpress /database:Northwind /pluralize
   /xml:Northwind.dbml

Dopo aver generato il file dbml, è possibile procedere e annotarlo con l'attributo classe e proprietà per descrivere il modo in cui le tabelle e le colonne vengono mappate a classi e proprietà. Dopo aver completato l'annotazione del file dbml, è possibile generare il modello a oggetti eseguendo il comando seguente:

C#

SqlMetal /namespace:nwind /code:Northwind.cs Northwind.dbml

Visual Basic

SqlMetal /namespace:nwind /code:Northwind.vb Northwind.dbml /language:vb

La firma di utilizzo SQLMetal è la seguente:

SqlMetal [options] [filename]

Di seguito è riportata una tabella che mostra le opzioni della riga di comando disponibili per SQLMetal.

Opzioni della riga di comando per SQLMetal

Opzione Descrizione
/server:<name> Indica il server a cui connettersi per accedere al database.
/database:<name> Indica il nome del database da cui leggere i metadati.
/user:<name> ID utente di accesso per il server.
/password:<name> Password di accesso per il server.
/views Estrarre le visualizzazioni del database.
/functions Estrarre funzioni di database.
/sprocs Estrarre stored procedure.
/code[:<filename>] Indica che l'output dello strumento è un file di origine di dichiarazioni di classe di entità.
/language:<language> Usare Visual Basic o C# (impostazione predefinita).
/xml[:<filename>] Indica che l'output degli strumenti è un file DBML che descrive i metadati del database e la prima approssimazione dei nomi di classe e proprietà.
/map[:<filename>] Indica che è necessario utilizzare un file di mapping esterno anziché attributi.
/pluralize Indica che lo strumento deve eseguire la lingua inglese pluralizzando o de pluralizzando l'euristica ai nomi delle tabelle per produrre nomi di classe e proprietà appropriati.
/namespace:<name> Indica lo spazio dei nomi in cui verranno generate le classi di entità.
/timeout:<seconds> Valore di timeout in secondi da usare per i comandi di database.

Nota Per estrarre i metadati da un file MDF, è necessario specificare il nome del file MDF dopo tutte le altre opzioni. Se si presuppone che non venga specificato alcun /serverlocalhost .

Informazioni di riferimento su DBML dello strumento generatore

Il file DBML (Database Mapping Language) è in primo luogo una descrizione dei metadati SQL per un determinato database. Viene estratto da SQLMetal esaminando i metadati del database. Lo stesso file viene usato anche da SQLMetal per generare un modello a oggetti predefinito per rappresentare il database.

Ecco un esempio prototipico della sintassi 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>

Gli elementi e i relativi attributi sono descritti di seguito.

Database

Si tratta dell'elemento più esterno nel formato XML. Questo elemento viene mappato in modo libero all'attributo Database nel DataContext generato.

Attributi del database

Attributo Type Predefinito Descrizione
Nome string Nessuno Nome del database. Se presente, e se si genera un oggetto DataContext, allegherà un attributo Database a tale attributo con questo nome. Usato anche come nome della classe DataContext se l'attributo di classe non è presente.
EntityNamespace Assoluta Nessuno Spazio dei nomi predefinito per le classi generate dagli elementi Type all'interno degli elementi Table. Se non viene specificato alcuno spazio dei nomi, le classi di entità vengono generate nello spazio dei nomi radice.
ContextNamespace string Nessuno Spazio dei nomi predefinito per la classe DataContext generata. Se non viene specificato alcuno spazio dei nomi, la classe DataContext viene generata nello spazio dei nomi radice.
Classe string Database.Name Nome della classe DataContext generata. Se non è presente, utilizzare l'attributo Name dell'elemento Database.
AccessModifier AccessModifier Pubblici Livello di accessibilità della classe DataContext generata. I valori validi sono Public, Protected, Internal e Private.
BaseType string "System.Data.Linq.DataContext" Tipo di base della classe DataContext .
Provider string "System.Data.Linq.SqlClient.Sql2005Provider" Il provider di DataContext, usare il provider Sql2005 come predefinito
ExternalMapping Boolean Falso Specificare se dbML viene usato per generare un file di mapping esterno.
Serializzazione SerializationMode SerializationMode.None Specificare se le classi di entità e DataContext generate sono serializzabili.

Attributi Sub-Element database

Sub-Element Tipo di elemento Intervallo di occorrenze Descrizione
<Tabella> Tabella 0-unbounded Rappresenta una tabella o una vista SQL Server di cui verrà eseguito il mapping a un singolo tipo o a una gerarchia di ereditarietà.
<Funzione> Funzione 0-unbounded Rappresenta una stored procedure SQL Server o una funzione db di cui verrà eseguito il mapping a un metodo nella classe DataContext generata.
<Connection> Connessione 0-1 Rappresenta la connessione al database che verrà utilizzata da DataContext .

Tabella

Questo elemento rappresenta una tabella di database (o una vista) di cui verrà eseguito il mapping a un singolo tipo o a una gerarchia di ereditarietà. Questo elemento esegue il mapping libero all'attributo Table nella classe di entità generata.

Attributi della tabella

Attributo Type Predefinito Descrizione
Nome string (obbligatorio) Nome della tabella all'interno del database. Funge da base del nome predefinito per l'adattatore di tabella, se necessario.
Membro string Table.Name Nome del campo membro generato per questa tabella all'interno della classe DataContext .
AccessModifier AccessModifier Pubblici Livello di accessibilità del riferimento Table<T> all'interno di DataContext. I valori validi sono Public, Protected, Internal e Private.

Attributi Sub-Element tabella

Sub-Element Tipo di elemento Intervallo di occorrenze Descrizione
<Tipo> Tipo 1-1 Rappresenta il tipo o la gerarchia di ereditarietà mappata a questa tabella.
<InsertFunction> TableFunction 0-1 Metodo per l'inserimento. Quando è presente, viene generato un metodo InsertT .
<UpdateFunction> TableFunction 0-1 Metodo per l'aggiornamento. Quando è presente, viene generato un metodo UpdateT .
<DeleteFunction> TableFunction 0-1 Metodo per l'eliminazione. Quando è presente, viene generato un metodo DeleteT .

Tipo

Questo elemento rappresenta una definizione di tipo per una forma di risultato table o stored procedure. Verrà creata una generazione di codice in un nuovo tipo CLR con le colonne e le associazioni specificate.

Il tipo può anche rappresentare un componente di una gerarchia di ereditarietà, con più tipi di mapping alla stessa tabella. In questo caso gli elementi Type vengono annidati per rappresentare le relazioni di ereditarietà padre-figlio e vengono differenziati nel database in base all'ereditarietà specificata da InheritanceCode .

Attributi di tipo

Attributo Type Predefinito Descrizione
Nome string (obbligatorio) Nome del tipo CLR da generare.
InheritanceCode string Nessuno Se questo tipo partecipa all'ereditarietà, può avere un codice di ereditarietà associato per distinguere tra i tipi CLR durante il caricamento di righe dalla tabella. Il tipo di cui InheritanceCode corrisponde al valore della colonna IsDiscriminator viene utilizzato per creare un'istanza dell'oggetto caricato. Se il codice di ereditarietà non è presente, la classe di entità generata è astratta.
IsInheritanceDefault Boolean Falso Se questo vale per un tipo in una gerarchia di ereditarietà, questo tipo verrà usato durante il caricamento di righe che non corrispondono a codici di ereditarietà definiti.
AccessModifier AccessModifier Pubblici Livello di accessibilità del tipo CLR creato. I valori validi sono : Public, Protected, Internal e Private.
ID string Nessuno Un tipo può avere un ID univoco. L'ID di un tipo può essere utilizzato da altre tabelle o funzioni. L'ID viene visualizzato solo nel file DBML, non nel modello a oggetti.
Idref string Nessuno IdRef viene usato per fare riferimento all'ID di un altro tipo. Se IdRef è presente in un elemento di tipo, l'elemento type deve contenere solo le informazioni IdRef . IdRef viene visualizzato solo nel file DBML, non nel modello a oggetti.

Tipi Sub-Element attributi

Sub-Element Tipo di elemento Intervallo di occorrenze Descrizione
<Colonna> Colonna 0-unbounded Rappresenta una proprietà all'interno di questo tipo che verrà associata a un campo nella tabella di questo tipo.
<Association Rules> Association Rules 0-unbounded Rappresenta una proprietà all'interno di questo tipo che verrà associata a un'estremità di una relazione di chiave esterna tra tabelle.
<Tipo> Sottotipo 0-unbounded Rappresenta i sottotipi di questo tipo all'interno di una gerarchia di ereditarietà.

Sottotipo

Questo elemento rappresenta un tipo derivato in una gerarchia di ereditarietà. Verrà generato in un nuovo tipo CLR con le colonne e le associazioni specificate in questo tipo. Non vengono generati attributi di ereditarietà per i sottotipi.

Rispetto a Type, gli elementi SubType non dispongono di AccessModifier perché tutti i tipi derivati devono essere pubblici. I sottotipi non possono essere riutilizzati da altre tabelle e funzioni, pertanto non sono presenti ID e IdRef .

Attributi SubType

Attributo Type Predefinito Descrizione
Nome string (obbligatorio) Nome del tipo CLR da generare.
EreditarietàCode string Nessuno Se questo tipo partecipa all'ereditarietà, può avere un codice di ereditarietà associato per distinguere tra i tipi CLR durante il caricamento di righe dalla tabella. Tipo il cui codice di ereditarietà corrisponde al valore della colonna IsDiscriminator viene usato per creare un'istanza dell'oggetto caricato. Se il codice di ereditarietà non è presente, la classe di entità generata è astratta.
IsInheritanceDefault Boolean Falso Se questo valore è true per un tipo in una gerarchia di ereditarietà, questo tipo verrà usato durante il caricamento di righe che non corrispondono ai codici di ereditarietà definiti.

Attributi Sub-Element SubType

Sub-Element Tipo di elemento Intervallo di occorrenze Descrizione
<Colonna> Colonna 0-unbounded Rappresenta una proprietà all'interno di questo tipo che verrà associata a un campo nella tabella di questo tipo.
<Association Rules> Association Rules 0-unbounded Rappresenta una proprietà all'interno di questo tipo a cui verrà associato una fine di una relazione di chiave esterna tra tabelle.
<Tipo> Sottotipo 0-unbounded Rappresenta i sottotipi di questo tipo all'interno di una gerarchia di ereditarietà.

Colonna

Questo elemento rappresenta una colonna all'interno di una tabella mappata a una proprietà (e a un campo di backup) all'interno di una classe. Non sarà presente alcun elemento Column per una fine di una relazione di chiave esterna, tuttavia, poiché è completamente rappresentata (su entrambe le estremità) dagli elementi Association.

Attributi di colonna

Attributi Type Predefinito Descrizione
Nome string Nessuno Il nome del campo del database a cui verrà eseguito il mapping della colonna.
Membro string Nome Nome della proprietà CLR da generare nel tipo contenente.
Archiviazione string _Membro Nome del campo di backup CLR privato che archivierà il valore di questa colonna. Non rimuovere l'archiviazione durante la serializzazione, anche se è predefinita.
AccessModifier AccessModifier Pubblici Livello di accessibilità della proprietà CLR creato. I valori validi sono: Public, Protected, Internal e Private.
Type string (obbligatorio) Nome del tipo della proprietà CLR e del campo di backup da creare. Questo può essere qualsiasi elemento da un nome completo a solo il nome diretto di una classe, purché il nome sarà infine nell'ambito quando viene compilato il codice generato.
DbType string Nessuno Tipo di SQL Server completo (inclusa l'annotazione, ad esempio NOT NULL) per questa colonna. Usato da LINQ to SQL se viene fornito per ottimizzare le query generate e per essere più specifiche quando si esegue CreateDatabase(). Serializzare sempre DbType.
IsReadOnly Boolean Falso Se IsReadOnly è impostato, non viene creato un setter di proprietà, ovvero le persone non possono modificare il valore di questa colonna usando tale oggetto.
IsPrimaryKey Boolean Falso Indica che questa colonna partecipa alla chiave primaria della tabella. Queste informazioni sono necessarie per LINQ to SQL di funzionare correttamente.
IsDbGenerated Boolean Falso Indica che i dati del campo vengono generati dal database. Questo è il caso principalmente per i campi AutoNumber e per i campi calcolati. Non è significativo assegnare valori a questi campi e pertanto sono automaticamente IsReadOnly.
Canbenull Boolean Nessuno Indica che il valore può contenere il valore Null. Se si desidera effettivamente usare valori Null in CLR, è comunque necessario specificare ClrType come T> nullable<.
UpdateCheck UpdateCheck Sempre (a meno che almeno un altro membro abbia impostato IsVersion , quindi Mai) Indica se LINQ to SQL deve usare questa colonna durante il rilevamento dei conflitti di concorrenza ottimistica. Normalmente tutte le colonne partecipano per impostazione predefinita, a meno che non vi sia una colonna IsVersion , che partecipa da sola. Può essere: Always, Never o WhenChanged (ovvero la colonna partecipa se il proprio valore è stato modificato).
IsDiscriminator Boolean Falso Indica se questo campo contiene il codice discriminatorio usato per la scelta tra tipi in una gerarchia di ereditarietà.
Expression string Nessuno Non influisce sull'operazione di LINQ to SQL, ma viene usata durante .CreateDatabase() come espressione SQL non elaborata che rappresenta l'espressione di colonna calcolata.
IsVersion Boolean Falso Indica che questo campo rappresenta un campo TIMESTAMP in SQL Server che viene aggiornato automaticamente ogni volta che la riga viene modificata. Questo campo può quindi essere usato per abilitare un rilevamento dei conflitti di concorrenza ottimistico più efficiente.
IsDelayLoaded Boolean Falso Indica che questa colonna non deve essere caricata immediatamente dopo la materializzazione dell'oggetto, ma solo quando si accede per la prima volta alla proprietà pertinente. Ciò è utile per campi memo di grandi dimensioni o dati binari in una riga che non è sempre necessaria.
AutoSync AutoSync If (IsDbGenerated && IsPrimaryKey) OnInsert;

Else if (IsDbGenerated) Always

Else Never

Specifica se la colonna viene sincronizzata automaticamente dal valore generato dal database. I valori validi per questo tag sono : OnInsert, Always e Never.

Association Rules

Questo elemento rappresenta una delle estremità di una relazione di chiave esterna. Per le relazioni uno-a-molti, si tratta di entityset<T> da un lato e di EntityRef<T> sul lato molti. Per le relazioni uno-a-uno, si tratta di entityRef<T> su entrambi i lati.

Si noti che non è necessario avere una voce di associazione su entrambi i lati di un'associazione. In questo caso, una proprietà verrà generata solo sul lato con la voce (formando una relazione unidirezionale).

Attributi di associazione

Attributo Type Predefinito Descrizione
Nome string (obbligatorio) Nome della relazione (in genere il nome del vincolo di chiave esterna). Questo può essere tecnicamente facoltativo, ma deve essere sempre generato dal codice per evitare ambiguità quando sono presenti più relazioni tra le stesse due tabelle.
Membro string Nome Nome della proprietà CLR da generare sul lato dell'associazione.
Archiviazione string Se OneToMany e Non IsForeignKey:

_OtherTable

Altro:

_TypeName(OtherTable)

Nome del campo sottostante CLR privato che archivierà il valore di questa colonna.
AccessModifier AccessModifier Pubblici Livello di accessibilità della proprietà CLR creata. I valori validi sono Public, Protected, Internal e Private.
ThisKey string Proprietà IsIdentity all'interno della classe contenitore Elenco delimitato da virgole delle chiavi su questo lato dell'associazione.
OtherTable string Vedere la descrizione. Tabella sull'altra estremità della relazione. In genere ciò può essere determinato dal runtime di LINQ to SQL tramite nomi di relazione corrispondenti, ma non è possibile per le associazioni unidirezionali o le associazioni anonime.
OtherKey string Chiavi primarie all'interno della classe esterna Elenco delimitato da virgole delle chiavi dall'altra parte dell'associazione.
IsForeignKey Boolean Falso Indica se si tratta del lato "figlio" della relazione, il lato molti di un elemento uno-a-molti.
RelationshipType RelationshipType OneToMany Indica se l'utente afferma che i dati correlati a questa associazione soddisfano i criteri di dati uno-a-uno o si adattano al caso più generale di uno-a-molti. Per uno-a-uno, l'utente afferma che per ogni riga sul lato chiave primaria ("uno") è presente una sola riga sul lato chiave esterna ("molti"). In questo modo verrà generato un oggetto EntityRef<T> sul lato "uno" anziché un EntitySet<T>. I valori validi sono OneToOne e OneToMany.
DeleteRule string Nessuno Usato per aggiungere il comportamento di eliminazione a questa associazione. Ad esempio, "CASCADE" aggiunge "ONDELETECASCADE" alla relazione FK. Se impostato su Null, non viene aggiunto alcun comportamento di eliminazione.

Funzione

Questo elemento rappresenta una stored procedure o una funzione di database. Per ogni nodo Funzione , viene generato un metodo nella classe DataContext .

Attributi della funzione

Attributo Type Predefinito Descrizione
Nome string (obbligatorio) Nome della stored procedure all'interno del database.
Metodo string Metodo Nome del metodo CLR da generare che consente la chiamata della stored procedure. Il nome predefinito per Method include elementi come [dbo]. rimosso da Name.
AccessModifier AccessModifier Pubblici Livello di accessibilità del metodo della stored procedure. I valori validi sono Public, Protected, Internal e Private.
HasMultipleResults Boolean Numero di tipi > 1 Specifica se la stored procedure rappresentata da questo nodo Funzione restituisce più set di risultati. Ogni set di risultati è una forma tabulare, può essere un tipo esistente o un set di colonne. Nel secondo caso, verrà creato un nodo Tipo per il set di colonne.
IsComposable Boolean Falso Specifica se la funzione o la stored procedure può essere composta in LINQ to SQL query. È possibile comporre solo le funzioni di database che non restituiscono void.

Attributi Sub-Element di funzione

Sub-Element Tipi di elemento Intervallo di occorrenze Descrizione
<Parametro> Parametro 0-unbounded Rappresenta i parametri in e out di questa stored procedure.
<ElementType> Tipo 0-unbounded Rappresenta le forme tabulari che la stored procedure corrispondente può restituire.
<Return> Return 0-1 Tipo scalare restituito di questa funzione db o stored procedure. Se Return è Null, la funzione restituisce void. Una funzione non può avere sia Return che ElementType.

TableFunction

Questo elemento rappresenta le funzioni di override CUD per le tabelle. La finestra di progettazione LINQ to SQL consente la creazione di metodi di override Insert, Update ed Delete per LINQ TO SQL e consente il mapping dei nomi delle proprietà di entità ai nomi dei parametri della stored procedure.

Il nome del metodo per le funzioni CUD è corretto, pertanto non esiste alcun attributo Method in DBML per gli elementi TableFunction . Ad esempio, per la tabella Customer, i metodi CUD vengono denominati InsertCustomer, UpdateCustomer e DeleteCustomer.

Una funzione table non può restituire una forma tabulare, pertanto non è presente alcun attributo ElementType nell'elemento TableFunction .

Attributi TableFunction

Attributo Type Predefinito Descrizione
Nome string (obbligatorio) Nome della stored procedure all'interno del database.
AccessModifier AccessModifier Privato Livello di accessibilità del metodo della stored procedure. I valori validi sono Public, Protected, Internal e Private.
HasMultipleResults Boolean Numero di tipi > 1 Specifica se la stored procedure rappresentata da questo nodo Funzione restituisce più set di risultati. Ogni set di risultati è una forma tabulare, può essere un tipo esistente o un set di colonne. Nel secondo caso, verrà creato un nodo Tipo per il set di colonne.
IsComposable Boolean Falso Specifica se la funzione o la stored procedure può essere composta in LINQ to SQL query. È possibile comporre solo le funzioni di database che non restituiscono void.

Attributi tableFunction Sub-Element

Sub-Elements Tipo di elemento Intervallo di occorrenze Descrizione
<Parametro> TableFunctionParameter 0-unbounded Rappresenta i parametri in e out di questa funzione di tabella.
<Return> TableFunctionReturn 0-1 Tipo scalare restituito di questa funzione di tabella. Se Return è Null, la funzione restituisce void.

Parametro

Questo elemento rappresenta un parametro stored procedure/funzione. I parametri possono passare dati in uscita e in uscita.

Attributi del parametro

Attributo Type Predefinito Descrizioni
Nome string (obbligatorio) Nome del database del parametro stored proc/function.
Parametro string Nome Nome CLR del parametro del metodo.
  string (obbligatorio) Nome CLR del parametro del metodo.
DbType string Nessuno Tipo di database del parametro stored proc/function.
Direction Parameterdirection In Direzione del flusso del parametro. Può essere uno di In, Out e InOut.

Return

Questo elemento rappresenta il tipo restituito di una stored procedure/funzione.

Attributi restituiti

Attributo Type Predefinito Descrizione
Type string (obbligatorio) Tipo CLR del risultato del processo/funzione archiviato.
DbType string Nessuno Tipo di database del risultato del processo/funzione archiviato.

TableFunctionParameter

Questo elemento rappresenta un parametro di una funzione CUD. I parametri possono passare dati e uscire. Ogni parametro viene mappato a una colonna Table a cui appartiene questa funzione CUD. Non sono presenti attributi Type o DbType in questo elemento perché le informazioni sul tipo possono essere ottenute dalla colonna a cui viene eseguito il mapping del parametro.

Attributi TableFunctionParameter

Attributo Type Predefinito Descrizione
Nome string (obbligatorio) Nome del database del parametro della funzione CUD.
Parametro string Nome Nome CLR del parametro del metodo.
Colonna string Nome Il nome della colonna a cui viene eseguito il mapping di questo parametro.
Direction Parameterdirection In Direzione del flusso del parametro. Può essere uno di In, Out o InOut.
Versione Versione Corrente Se PropertyName fa riferimento alla versione corrente o originale di una determinata colonna. Applicabile solo durante l'override dell'aggiornamento . Può essere Corrente o Originale.

TableFunctionReturn

Questo elemento rappresenta un tipo restituito di una funzione CUD. Contiene effettivamente solo il nome della colonna mappato al risultato della funzione CUD. Le informazioni sul tipo della restituzione possono essere ottenute dalla colonna.

Attributo TableFunctionReturn

Attrobite Type Predefinito Descrizione
Colonna string Nessuno Nome della colonna a cui viene eseguito il mapping del restituito.

Connessione

Questo elemento rappresenta i parametri di connessione del database predefiniti. Ciò consente la creazione di un costruttore predefinito per il tipo DataContext che sa già come connettersi a un database.

Esistono due tipi di connessioni predefinite, una con una connessione direttaString e una che legge da App.Settings.

Attributi di connessione

Attributo Type Predefinito Descrizione
UseApplicationSettings Boolean Falso Determina se usare un file App.Settings o ottenere le impostazionidell'applicazione da una connessione direttaString.
ConnectionString string Nessuno Stringa di connessione da inviare al provider di dati SQL.
SettingsObjectName string Impostazioni Oggetto App.Settings da cui recuperare le proprietà.
SettingsPropertyName string ConnectionString Proprietà App.Settings che contiene ConnectionString.

Entità a più livelli

Nelle applicazioni a due livelli, un singolo DataContext gestisce query e aggiornamenti. Tuttavia, per le applicazioni con livelli aggiuntivi, è spesso necessario usare istanze di DataContext separate per query e aggiornamenti. Ad esempio, nel caso di applicazioni ASP.NET, le query e l'aggiornamento vengono eseguite per richieste separate al server Web. Di conseguenza, è impraticabile usare la stessa istanza di DataContext tra più richieste. In questi casi, un'istanza di DataContext deve essere in grado di aggiornare gli oggetti non recuperati. Il supporto dell'entità a più livelli in LINQ to SQL offre tale funzionalità tramite il metodo Attach().

Ecco un esempio di come è possibile modificare un oggetto Customer usando un'istanza di DataContext diversa:

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()

Nelle applicazioni a più livelli l'intera entità spesso non viene inviata tra livelli per semplicità, interoperabilità o privacy. Ad esempio, un fornitore può definire un contratto dati per un servizio Web diverso dall'entità Order usata nel livello intermedio. Analogamente, una pagina Web può mostrare solo un subset dei membri di un'entità Employee. Di conseguenza, il supporto multilivello è progettato per supportare tali casi. Solo i membri appartenenti a una o più delle categorie seguenti devono essere trasportati tra i livelli e impostati prima di chiamare Attach().

  1. Membri che fanno parte dell'identità dell'entità.
  2. Membri modificati.
  3. Membri che partecipano al controllo della concorrenza ottimistica.

Se viene usato un timestamp o una colonna numero di versione per il controllo della concorrenza ottimistica, è necessario impostare il membro corrispondente prima di chiamare Attach().If a timestamp or a version number column is used for optimistic concorrenza check, the corresponding member must be set before calling Attach(). I valori per altri membri non devono essere impostati prima di chiamare Attach(). LINQ to SQL usa aggiornamenti minimi con controlli di concorrenza ottimistica, ovvero un membro non impostato o controllato per la concorrenza ottimistica viene ignorato.

I valori originali necessari per i controlli di concorrenza ottimistica possono essere conservati usando un'ampia gamma di meccanismi esterni all'ambito delle API di LINQ to SQL. Un'applicazione ASP.NET può usare uno stato di visualizzazione (o un controllo che usa lo stato di visualizzazione). Un servizio Web può usare DataContract per un metodo di aggiornamento per garantire che i valori originali siano disponibili per l'elaborazione degli aggiornamenti. Nell'interesse dell'interoperabilità e della generalità, LINQ to SQL non determina la forma dei dati scambiati tra livelli o i meccanismi utilizzati per il round trip dei valori originali.

Le entità per l'inserimento e l'eliminazione non richiedono il metodo Attach(). I metodi usati per le applicazioni a due livelli: Table.Add() e Table.Remove() possono essere usati per l'inserimento e l'eliminazione. Come nel caso degli aggiornamenti a due livelli, un utente è responsabile della gestione dei vincoli di chiave esterna. Un cliente con ordini non può essere rimosso senza gestire gli ordini se nel database è presente un vincolo di chiave esterna che impedisce l'eliminazione di un cliente con ordini.

LINQ to SQL gestisce anche l'allegato delle entità per gli aggiornamenti in modo transitivo. L'utente crea essenzialmente il grafico oggetto di pre-aggiornamento come desiderato e chiama Attach(). Tutte le modifiche possono quindi essere "riprodotte" nel grafico associato per eseguire gli aggiornamenti necessari, come illustrato di seguito:

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()

Mapping esterno

Oltre al mapping basato su attributi, LINQ to SQL supporta anche il mapping esterno. Il formato più comune di mapping esterno è un file XML. I file di mapping consentono scenari aggiuntivi in cui è consigliabile separare il mapping dal codice.

DataContext fornisce un costruttore aggiuntivo per fornire un oggetto MappingSource. Una forma di MappingSource è un oggetto XmlMappingSource che può essere costruito da un file di mapping XML.

Di seguito è riportato un esempio di come usare il file di mapping:

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   )

Ecco un frammento corrispondente del file di mapping che mostra il mapping per la classe Product . Mostra la classe Product nello spazio dei nomi Mapping mappata alla tabella Products nel database Northwind . Gli elementi e gli attributi sono coerenti con i nomi e i parametri degli attributi.

<?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>

Note e supporto delle funzioni di NET Framework

I paragrafi seguenti forniscono informazioni di base sul supporto dei tipi di LINQ to SQL e sulle differenze rispetto a .NET Framework.

Tipi primitivi

Implementato

  • Operatori di confronto e aritmetici
  • Operatori di spostamento: << e >>
  • La conversione tra caratteri e numerici viene eseguita da UNICODE/NCHAR; in caso contrario, viene usata la funzione CONVERT di SQL.

Non implementato

  • <Tipo>. Parse
  • Le enumerazioni possono essere usate e mappate a numeri interi e stringhe in una tabella. Per quest'ultimo vengono usati i metodi Parse e ToString( ).

Differenza da .NET

  • L'output di ToString per double usa CONVERT(NVARCHAR(30), @x, 2) in SQL, che usa sempre 16 cifre e "Notazione scientifica". Ad esempio: "0.0000000000000000e+000" per 0, quindi non assegna la stessa stringa di . Convert.ToString()di NET.

System.String

Implementato

  • Metodi non statici:

    • Length, Substring, Contains, StartsWith, EndsWith, IndexOf, Insert, Remove, Replace, Trim, ToLower, ToUpper, LastIndexOf, PadRight, PadLeft, Equals, CompareTo. Tutte le firme sono supportate, tranne quando accettano il parametro StringComparison e così via, come descritto di seguito.
  • Metodi statici:

       Concat(...)               all signatures
       Compare(String, String)
       String (indexer) 
       Equals(String, String)
    
  • Costruttore:

        String(Char, Int32)
    
  • Operatori:

      +, ==, != (+, =, and <> in Visual Basic)
    

Non implementato

  • Metodi che accettano o producono una matrice di caratteri.

  • Metodi che accettano un oggetto CultureInfo/StringComparison/IFormatProvider.

  • Statico (condiviso in 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)
    

Restrizioni/Differenza da .NET

SQL usa regole di confronto per determinare l'uguaglianza e l'ordinamento delle stringhe. Questi valori possono essere specificati in un'istanza di SQL Server, in un database, in una colonna di tabella o in un'espressione.

Le traduzioni delle funzioni implementate finora non modificano le regole di confronto o specificano regole di confronto diverse nelle espressioni tradotte. Pertanto, se le regole di confronto predefinite non fanno distinzione tra maiuscole e minuscole, funzioni come CompareTo o IndexOf possono fornire risultati che differiscono dalle funzioni .NET (con distinzione tra maiuscole e minuscole).

I metodi StartsWith(str)/EndsWith(str) presuppongono che l'argomento str sia una costante o un'espressione valutata nel client. Ciò significa che attualmente non è possibile usare una colonna per str.

System.Math

Implementazione di metodi statici

  • Tutte le firme:
    • Abs, Acos, Asin, Atan, Atan2, BigMul, Ceiling, Cos, Cosh, Exp, Floor, Log, Log10, Max, Min, Pow, Sign, Sinh, Sqrt, Tan, Tanh o Truncate.

Non implementato

  • IEEERemainder.
  • DivRem ha un parametro out, pertanto non è possibile usarlo in un'espressione. Le costanti Math.PI e Math.E vengono valutate nel client, quindi non hanno bisogno di una traduzione.

Differenza da .NET

La conversione della funzione .NET Math.Round è la funzione SQL ROUND. La traduzione è supportata solo quando viene specificato un overload che indica il valore di enumerazione MidpointRounding . MidpointRounding.AwayFromZero è il comportamento di SQL e MidpointRounding.ToEven indica il comportamento CLR.

System.Convert

Implementato

  • Metodi del formato A<Type1>(<Type2> x) dove Type1, Type2 è uno dei seguenti:
    • bool, byte, char, DateTime, decimal, double, float, Int16, Int32, Int64 o string.
  • Il comportamento è lo stesso di un cast:
    • Per ToString(Double) è disponibile codice speciale per ottenere la precisione completa.
    • Per la conversione Int32/Char, LINQ to SQL usa la funzioneNCHARUNICODE/ di SQL.
    • In caso contrario, la traduzione è CONVERT.

Non implementato

  • ToSByte, UInt16, 32, 64: questi tipi non esistono in SQL.

    To<integer type>(String, Int32) 
    ToString(..., Int32)       any overload ending with an Int32 toBase
    IsDBNull(Object)
    GetTypeCode(Object)
    ChangeType(...)
    
  • Versioni con il parametro IFormatProvider .

  • Metodi che coinvolgono una matrice (To/FromBase64CharArray, To/FromBase64String).

System.TimeSpan

Implementato

  • Costruttori:

       TimeSpan(Long)
       TimeSpan (year, month, day)
       TimeSpan (year, month, day, hour, minutes, seconds)
       TimeSpan (year, month, day, hour, minutes, seconds, milliseconds)
    
  • Operatori:

       Comparison operators: <,==, and so on in C#; <, =, and so on in Visual Basic
    
       +, -
    
  • Metodi statici (condivisi in Visual Basic):

       Compare(t1,t2)
    
  • Metodi/proprietà non statici (Istanza):

       Ticks, Milliseconds, Seconds, Hours, Days
       TotalMilliseconds, TotalSeconds, TotalMinutes, TotalHours, TotalDays,
       Equals, CompareTo(TimeSpan)
       Add(TimeSpan), Subtract(TimeSpan)
       Duration() [= ABS], Negate()
    

Non implementato

   ToString()
   TimeSpan FromDay(Double), FromHours,   all From Variants
   TimeSpan Parse(String)

System.DateTime

Implementato

  • Costruttori:

       DateTime(year, month, day)
       DateTime(year, month, day, hour, minutes, seconds)
       DateTime(year, month, day, hour, minutes, seconds, milliseconds)
    
  • Operatori:

       Comparisons
       DateTime – DateTime (gives TimeSpan)
       DateTime + TimeSpan (gives DateTime)
       DateTime – TimeSpan (gives DateTime)
    
  • Metodi statici (condivisi):

       Add(TimeSpan), AddTicks(Long),
       AddDays/Hours/Milliseconds/Minutes (Double)
       AddMonths/Years(Int32)
       Equals
    
  • Metodi/proprietà non statici (Istanza):

       Day, Month, Year, Hour, Minute, Second, Millisecond, DayOfWeek
       CompareTo(DateTime)
       TimeOfDay()
       Equals
       ToString()
    

Differenza da .NET

I valori datetime di SQL vengono arrotondati a .000, .003 o 007 secondi, quindi è meno preciso di quelli di .NET.

L'intervallo di datetime di SQL inizia il 1° gennaio 1753.

SQL non dispone di un tipo predefinito per TimeSpan. Usa metodi DATEDIFF diversi che restituiscono interi a 32 bit. Uno è DATEDIFF(DAY,...), che dà il numero di giorni; un altro è DATEDIFF(MILLISECOND,...), che fornisce il numero di millisecondi. Se DateTimes supera i 24 giorni di distanza, viene restituito un errore. Al contrario, .NET usa interi a 64 bit e misura timeSpan nei tick.

Per ottenere il più vicino possibile alla semantica .NET in SQL, LINQ to SQL converte TimeSpans in interi a 64 bit e usa i due metodi DATEDIFF indicati in precedenza per calcolare il numero di tick tra due date.

Datetime UtcNow viene valutato nel client quando la query viene convertita (come qualsiasi espressione che non coinvolge dati del database).

Non implementato

   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

Supporto per il debug

DataContext fornisce metodi e proprietà per ottenere il codice SQL generato per le query e l'elaborazione delle modifiche. Questi metodi possono essere utili per comprendere LINQ to SQL funzionalità e per il debug di problemi specifici.

Metodi DataContext per ottenere SQL generato

Membro Scopo
Log Stampa SQL prima dell'esecuzione. Vengono illustrati i comandi query, insert, update, delete. Utilizzo:

C#

db.Log = Console.Out;

Visual Basic

db.Log = Console.Out

GetQueryText(query) Restituisce il testo della query senza eseguirlo. Utilizzo:

C#

Console.WriteLine(db.GetQueryText(db.Customers));

Visual Basic

Console.WriteLine(db.GetQueryTest(db.Customers))

GetChangeText() Restituisce il testo dei comandi SQL per inserimento/aggiornamento/eliminazione senza eseguirli. Utilizzo:

C#

Console.WriteLine(db.GetChangeText());

Visual Basic

Console.WriteLine(db.GetChangeText())