Entity Framework
Giovanni Lagorio
[email protected]
ADO Entities Framework
• E’ un ORM
– Si pensa/lavora in termini di oggetti, non
colonne/righe
• Permette di lavorare a livello di Modello E/R
(ereditarietà, relazioni molti-a-molti, ...)
• Si può interrogare via:
– Entities SQL (ESQL), per esempio:
SELECT VALUE c FROM E.Contacts AS c
WHERE c.FirstName='Pippo'
– LINQ to Entities
La sintassi è quella che vi aspettate
Storia - Panoramica
• .NET 3.5
– Unica modalità di lavoro: DataBase First
• Si mappa un modello E/R su un DB esistente
– Descrizione del modello in XML (file EDMX)
• Il designer in VS «nasconde» XML
– Le entità devono estendere EntityObject
– ObjectContext API
• .NET 4
– Model First
• Si genera il DB a partire da un modello E/R
– Supporto ai POCO (Plain Old CLR Object)
• Fra .NET 4 e .NET 4.5
– Code First (Code Based sarebbe più appropriato...)
• Si basa tutto sul codice (no VS designer, no XML)
• Convention over Configuration (AKA Coding by Convention)
– DbContext API
• Stanno lavorando a CF Migrations
http://msdn.microsoft.com/en-us/data/jj591621
Si lavora rispetto
a un Entity Data
Model che
determina come
tradurre le
query, rendere
persistenti gli
oggetti, ecc.
Mapping
• Descritto da tre file XML in DB/Model First
– Ma VS li nasconde abbastanza
• Implicito (=dedotto dalla classi) in Code First
Modello
Concettuale
(E/R)
.CSDL
Conceptual Schema
Definition Language
Store
Mapping
Con Code First, Classi C#Schema
.MSL
.SSDL
Mapping
Specification
Language
Store Schema
Definition
Language
(tipicamente)
Oggetti
DB Relazionale
Code First (per davvero): C#Db
Esempio preso da:
http://msdn.microsoft.com/en-us/data/jj193542
1. Si parte dal modello (=classi C#)
2. Si aggiunge il riferimento a EF (via NuGet)
– Parentesi: gli App.Config/Web.Config
3. Si crea un contesto
4. Si possono leggere/scrivere i dati
Modello
public class Blog {
public Blog() { this.Posts = new List<Post>(); }
public int BlogId { get; set; }
public string Name { get; set; }
public virtual List<Post> Posts { get; set; }
}
public class Post {
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
Contesto
using System.Data.Entity;
// ...
public class BloggingContext : DbContext {
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
Lettura/scrittura
using (var db = new BloggingContext()) {
// Create and save a new Blog
Console.Write("Enter a name for a new Blog: ");
var name = Console.ReadLine();
var blog = new Blog {Name = name};
db.Blogs.Add(blog);
db.SaveChanges();
// Display all Blogs from the database
var query = from b in db.Blogs
orderby b.Name
select b;
Console.WriteLine("All blogs in the database:");
foreach (var item in query)
Console.WriteLine(item.Name);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
Facciamoci alcune domande
• Ok, funziona, ma dove scrive i dati?!?!
– Come posso cambiare il default?
• Come decide lo schema del DB?
– Come posso cambiare il default?
• Cos’è un contesto? (in questo contesto )
• Quando carico un Blog, si porta dietro anche tutti i post?
In breve:
1. Convention over configuration
2. Data Annotation (=Custom attribute)/Fluent API
3. Un oggetto che ci permette di «dialogare» col DB (è
UoW+IdentityMap+Repository+...)
4. Sni...
Quale DB (e DB Server)?
Per convenzione, DbContext crea un DB automaticamente (vedere
http://msdn.microsoft.com/en-us/data/jj592674)
Dove? Se c’è una connection-string (nel config) che si chiama come il contesto (semplice o
fully-qualified), usa quelle info, altrimenti per il nome del DB usa il nome (fully-qualified) del
contesto e si collega a SQL Express/LocalDb
Quale dei due? Il NuGet package controlla se
• SQL Express è disponibile
• Altrimenti prova LocalDb
e crea il file di configurazione (se non esiste già) che specifica quale usare
In labo, potete vedere cosa succede
collegandovi a .\SQLEXPRESS (con Windows Authentication) tramite
– la finestra (di VS) SQL Server Object Explorer
– SQL Server Management Studio
Vi sembra sensato?
Costruttori (non default) di DbContext
• Al costruttore di DbContext si può passare:
– Il nome del DB
– Il nome di una connection string (che va specificata in
<connectionStrings> nel file di configurazione)
– Direttamente una CS
– ... altro (vedere MSDN per i dettagli)
• Per specificare se creare il DB (se non c’è già, se è
diverso, ...) e/o inizializzarlo, si usano i Database
Initializers tramite
– File di configurazione (http://msdn.microsoft.com/en-US/data/jj556606)
– Il metodo Database.SetInitializer (...prox slide)
Database Initializers
http://msdn.microsoft.com/en-us/library/gg679461%28v=vs.103%29.aspx
Per esempio,
• Database.SetInitializer(…);
– Il default:
new CreateDatabaseIfNotExists<BloggingContext>()
– new DropCreateDatabaseAlways<BloggingContext>()
– new
DropCreateDatabaseIfModelChanges<BloggingContext>()
• Database.SetInitializer<BloggingContext>(null);
evita che il DB venga creato in automatico
Se si usa Migrations, anche MigrateDatabaseToLatestVersion
(http://msdn.microsoft.com/en-us/library/hh829293%28v=vs.103%29.aspx)
Parentesi sulle CS
• Due tipi (http://msdn.microsoft.com/en-us/data/jj592674):
– Quelle «vere» ai DB, usate da Code First
• Indicano il DB Server, il nome del DB, tipo di autenticazione, ...
Esempio:
"Data Source=.\SQLEXPRESS;Initial Catalog=BarDB;Integrated Security=SSPI;
MultipleActiveResultSets=True“
• Importante specificare MultipleActiveResultSets=True
– Quelle «dell’EF» usate da DB/Model First
• Contengono i riferimenti al modello (che viene descritto da tre
XML, «inscatolati» nell’EDMX generato da VS)
• E una CS «vera»
Esempio: "metadata=res://*/Northwind.csdl| res://*/Northwind.ssdl|
res://*/Northwind.msl; provider=System.Data.SqlClient; provider connection
string= "Data Source=.\sqlexpress; Initial Catalog=Northwind; Integrated
Security=True; MultipleActiveResultSets=True""
Come viene ottenuto il modello?
• Solo l’idea... tutti i dettagli su MSDN
http://msdn.microsoft.com/en-us/library/system.data.entity.modelconfiguration.conventions%28v=vs.103%29.aspx
• Alcune convenzioni (i dettagli in seguito...):
– A partire dal contesto, Type discovery «scopre» i tipi coinvolti
• In base a cosa compare nel vostro contesto, compresa chiusura transitiva di
uso/ereditarietà
– Chiave (obbligatoria, diventa quella primaria su DB): ‘Id’ o ‘<Typename>Id’,
tipo integrale o GUID
– Chiavi esterne: ‘<navigation property name><principal primary key property
name>’, ‘<principal class name><primary key property name>’, o ‘<principal
primary key property name>’
– Vincoli su chiavi esterne: in base alle proprietà di navigazione, molteplicità
comprese (T o ICollection<T> ?), chiave esterna nullabile o meno, ...
• Al momento (EF5) le convenzioni possono essere tolte, ma non aggiunte
(dalla prossima versione, forse...)
http://msdn.microsoft.com/en-us/data/jj679962
Nel nostro esempio...
In questa slide ho rimosso i vari public...
CREATE TABLE [dbo].[Blogs] (
[BlogId] INT IDENTITY (1, 1) NOT NULL,
[Name]
NVARCHAR (MAX) NULL,
CONSTRAINT [PK_dbo.Blogs] PRIMARY KEY
CLUSTERED ([BlogId] ASC)
);
class Blog {
Blog() {
this.Posts = new List<Post>();
}
int BlogId { get; set; }
CREATE TABLE [dbo].[Posts] (
string Name { get; set; }
[PostId] INT IDENTITY (1, 1) NOT NULL,
virtual List<Post> Posts
[Title]
NVARCHAR (MAX) NULL,
{ get; set; }
[Content] NVARCHAR (MAX) NULL,
}
[BlogId] INT NOT NULL,
class Post {
int PostId { get; set; }
string Title { get; set; }
string Content { get; set; }
int BlogId { get; set; }
virtual Blog Blog { get; set; }
}
Identity in
SQL Server =
«autoincr»
CONSTRAINT [PK_dbo.Posts] PRIMARY KEY
CLUSTERED ([PostId] ASC),
CONSTRAINT [FK_dbo.Posts_dbo.Blogs_BlogId] FOREIGN
KEY ([BlogId]) REFERENCES
[dbo].[Blogs] ([BlogId]) ON DELETE CASCADE
);
CREATE NONCLUSTERED INDEX [IX_BlogId]
ON [dbo].[Posts]([BlogId] ASC);
Proviamo a modificare l’esempio...
public class Blog {
public Blog() { this.Posts = new List<Post>(); }
public int MyKey { get; set; }
public string Name { get; set; }
public virtual List<Post> Posts { get; set; }
}
public class Post {
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
..... BOOM! Perché?
Data Annotation: [Key] (1/2)
Così non scoppia... ma... (notare
anche la molteplicità e l’assenza
del cascade)
CREATE TABLE [dbo].[Blogs] (
[MyKey] INT
IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_dbo.Blogs] PRIMARY KEY CLUSTERED ([MyKey] ASC)
);
public class Blog {
public Blog() {
this.Posts = new List<Post>();
}
CREATE TABLE [dbo].[Posts] (
[PostId]
INT
IDENTITY (1, 1) NOT NULL,
[Title]
NVARCHAR (MAX) NULL,
[Content]
NVARCHAR (MAX) NULL,
[BlogId]
INT
NOT NULL,
[Blog_MyKey] INT
NULL,
CONSTRAINT [PK_dbo.Posts] PRIMARY KEY CLUSTERED ([PostId] ASC),
CONSTRAINT [FK_dbo.Posts_dbo.Blogs_Blog_MyKey]
FOREIGN KEY ([Blog_MyKey]) REFERENCES [dbo].[Blogs] ([MyKey])
);
[Key]
public int MyKey { get; set; }
public string Name { get; set; }
public virtual List<Post> Posts
{ get; set; }
}
public class Post {
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog
{ get; set; }
}
CREATE NONCLUSTERED INDEX [IX_Blog_MyKey]
ON [dbo].[Posts]([Blog_MyKey] ASC);
Data Annotation: [Key] (2/2)
Così OK (0-a-molti)
public class Blog {
public Blog() {
this.Posts = new List<Post>();
}
[Key]
public int MyKey { get; set; }
public string Name { get; set; }
public virtual List<Post> Posts
{ get; set; }
}
public class Post {
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int? BlogMyKey { get; set; }
public virtual Blog Blog
{ get; set; }
}
CREATE TABLE [dbo].[Blogs] (
[MyKey] INT
IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_dbo.Blogs] PRIMARY KEY CLUSTERED ([MyKey] ASC)
);
CREATE TABLE [dbo].[Posts] (
[PostId]
INT
IDENTITY (1, 1) NOT NULL,
[Title]
NVARCHAR (MAX) NULL,
[Content]
NVARCHAR (MAX) NULL,
[BlogMyKey] INT
NULL,
CONSTRAINT [PK_dbo.Posts] PRIMARY KEY CLUSTERED ([PostId] ASC),
CONSTRAINT [FK_dbo.Posts_dbo.Blogs_BlogMyKey]
FOREIGN KEY ([BlogMyKey]) REFERENCES [dbo].[Blogs] ([MyKey])
);
CREATE NONCLUSTERED INDEX [IX_BlogMyKey]
ON [dbo].[Posts]([BlogMyKey] ASC);
Data Annotation & Fluent API
I due modi per configurare i vari aspetti del modello sono:
• Data Annotation applicate alle classi entità (e/o membri delle)
= custom-attribute definiti in System.ComponentModel.DataAnnotations
http://msdn.microsoft.com/en-us/data/jj591583
– Es: [Key], [Required], [MaxLength(50)], [MinLength(3)], [NotMapped],
[Table("foo")], [Column("bar", TypeName="ntext")], ...
– Alcune non sono spefiche di EF, per esempio StringLength (mentre
Max/MinLength lo sono…)
• Fluent API = API «molto leggibili», chiamate da (un override di)
OnModelCreating di DbContext
http://msdn.microsoft.com/en-us/data/jj591617
– Es:
mb.Entity<Department>().Property(t => t.Name).IsRequired();
mb.Entity<Department>().Ignore(t => t.Budget);
Alcune cose sono possibili solo con Fluent API (che dovrebbe avere tutto a
parte MinLength)
Data Annotation o Fluent API?
Questione di gusti...
IMHO, le entità è meglio che siano (il più possibile)
persistence ignorant quindi:
• Data Annotation per le caratteristiche del (modello
del) dominio
– Richiesto vs opzionale, lunghezza massima, ...
• Fluent API per il mapping sul DB
– Nomi di tabelle, colonne, tipi di dato, cascade, ...
Per esempio, continuando con Blog e Post...
Esempio con DA e FluentAPI (1/2)
public class Blog {
public Blog() { this.Posts = new List<Post>(); }
[Key]
public int Identifier { get; set; }
[StringLength(50)]
[Required]
public string Name { get; set; }
public class BloggingContext : DbContext {
//...
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(DbModelBuilder mb) {
var blobEntity = mb.Entity<Blog>();
blobEntity.ToTable("BB");
blobEntity.Property(b => b.Identifier)
.HasColumnName("MyId");
blobEntity.Property(b => b.RowVersion)
.IsRowVersion();
// implies: .IsConcurrencyToken();
mb.Entity<Post>().HasRequired(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogId)
.WillCascadeOnDelete(false);
}
public virtual List<Post> Posts { get; set; }
public byte [] RowVersion { get; set; }
}
public class Post {
public int Id { get; set; }
public string Title { get; set; }
[StringLength(50)]
public string Content { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
}
È possibile fattorizzare le configurazioni delle singole entità
estendendo EntityTypeConfiguration<TEntity>
e aggiungendo le istanze a mb.Configurations in OnModelCreating
Esempio con DA e FluentAPI (2/2)
CREATE TABLE [dbo].[BB] (
[MyId]
INT
IDENTITY (1, 1) NOT NULL,
[Name]
NVARCHAR (50) NOT NULL,
[RowVersion] ROWVERSION
NOT NULL,
CONSTRAINT [PK_dbo.BB] PRIMARY KEY CLUSTERED ([MyId] ASC)
);
CREATE TABLE [dbo].[Posts] (
[Id]
INT
IDENTITY (1, 1) NOT NULL,
[Title]
NVARCHAR (MAX) NULL,
[Content] NVARCHAR (50) NULL,
[BlogId] INT
NOT NULL,
CONSTRAINT [PK_dbo.Posts] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_dbo.Posts_dbo.BB_BlogId] FOREIGN KEY ([BlogId])
REFERENCES [dbo].[BB] ([MyId])
);
CREATE NONCLUSTERED INDEX [IX_BlogId]
ON [dbo].[Posts]([BlogId] ASC);
Code (non proprio) First: DBC#
Non indispensabile, ma molto comodo: EF Power
Tools (estensione di VS):
http://visualstudiogallery.msdn.microsoft.com/72a60b14-1581-4b9b-89f2-846072eff19d
1. Si parte dal DB
2. Si aggiunge il riferimento a EF
3. Si generano le classi facendo il «reverse
engineering» del DB
4. Si leggono/scrivono i dati (come prima)
Passo 1: Creazione del DB...
CREATE TABLE [dbo].[Blogs] (
[BlogId] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (200) NULL,
[Url] NVARCHAR (200) NULL,
CONSTRAINT [PK_dbo.Blogs] PRIMARY KEY CLUSTERED ([BlogId] ASC)
);
CREATE TABLE [dbo].[Posts] (
[PostId] INT IDENTITY (1, 1) NOT NULL,
[Title] NVARCHAR (200) NULL,
[Content] NTEXT NULL,
[BlogId] INT NOT NULL,
CONSTRAINT [PK_dbo.Posts] PRIMARY KEY CLUSTERED ([PostId] ASC),
CONSTRAINT [FK_dbo.Posts_dbo.Blogs_BlogId] FOREIGN KEY ([BlogId])
REFERENCES [dbo].[Blogs] ([BlogId]) ON DELETE CASCADE
);
Passo 3: DB  C#
• Dal menù contestuale «Entity Framework» di un
progetto (compare solo se avete installato i Power
Tools): «Reverse engineer Code First»
Esempio completo:
http://msdn.microsoft.com/en-us/data/jj200620
Intellitrace
• In generale, è possibile usarlo per
«sbirciare» il dialogo col DB
• Vanno registrati/guardati gli eventi
che riguardano ADO.NET
DbContext
• Il dialogo con il DB avviene tramite un db-context che
– Gestisce connessioni e transazioni
– Traduzione di query/comandi
– Materializza gli oggetti e tiene traccia delle modifiche,
inserimenti e cancellazioni
• Tutte le modifiche vengono salvate con un’unica chiamata a
SaveChanges
• Corrisponde alla Unit of Work dell’omonimo pattern
(http://martinfowler.com/eaaCatalog/unitOfWork.html)
• Si occupa di fare tutte le INSERT/UPDATE/DELETE, nell’ordine giusto, in una
transazione
Entità
Oggetti
DbContext:
IDisposable
DB
Dobbiamo considerare un po’ di cose...
Recuperare le entità
• (le classi che estendono) DbContext espongono i DbSet degli insieme di
entità
• DbSet<T> implementa (fra le altre cose) IQueryable<T>, quindi è «LINQaware»
– Caso particolare: Find recupera un’entità dalla chiave (=null se non la trova)
Find(x) ≈ Where(e=>e.Key==x).SingleOrDefault()
ma cerca anche in Local (ObservableCollection di DbSet<>)
using (var db = new BloggingContext()) {
var firstOrNull = db.Blogs.FirstOrDefault();
var b1 = db.Blogs.First();
var b2 = db.Blogs.Find(2);
var aa1 = db.Blogs.Where(b => b.Name == "aa").Single();
var aa2 = db.Blogs.Single(b => b.Name == "aa");
var blogsA = db.Blogs.Where(b => b.Name.StartsWith("a")).ToList();
}
• Il contesto tiene traccia delle entità caricate e implementa il pattern
IdentityMap
– Ovvero, stessa chiave = stesso oggetto in memoria
Tracking di entità
• Gli oggetti/entità “controllati” (tracked) da un context
sono detti Attached (al db-context)
– Un oggetto può essere controllato da un solo contesto
– Un grafo di oggetti è sempre completamente Attached o
completamente Detached
• Per ognuno di questi, c’è un DbEntityEntry<TEntity>
corrispondente che
– Si può recuperare tramite il metodo Entry e
– espone le proprietà (read-only):
•
•
•
•
•
TEntity Entity
DbPropertyValues CurrentValues
DbPropertyValues OriginalValues
EntityState State
...altro...
Entity State
Ogni entità è sempre in uno dei cinque stati
definiti dall’enumeration EntityState:
– Unchanged, non ha subito modifiche (in memoria)
– Added, verrà inserito nel DB da SaveChanges
– Deleted, verrà cancellato da SaveChanges
– Modified, verrà aggiornato da SaveChanges
– Detached
http://msdn.microsoft.com/en-us/data/jj592676
Relazioni 1-n
• Esposte tramite due proprietà di navigazione e, opzionalmente, una proprietà FK
(consiglio: esponete sempre la FK)
– «Lato 1» : collezione di oggetti del «Lato n»
– «Lato n» : riferimento a oggetto del «Lato 1»
– «Lato n» : chiave (esterna) dell’oggetto «Lato 1»
• Per creare/rimuovere un’associazione, basta cambiarne una delle tre
– Sistemare le rimanenti è chiamato relationship fix-up (e lo fa EF in automatico)
public class Blog {
public int BlogId { get; set; }
public string Name { get; set; }
public virtual List<Post> Posts { get; set; } }
public class Post {
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; } }
Change tracking & relationship fix-up
• Come? Due possibilità
– Snapshot
– Proxy; solo su classi pubbliche e non-sealed dove
• Tutte le proprietà virtual, con get/set pubblici
• Tutte le prorietà di navigazione verso «molti» hanno tipo
ICollection<T>
– Nota: non serve inizializzarle nel costruttore (lo fa EF)
• Quando?
– Snapshot: tramite ChangeTracker.DetectChanges()
• tipicamente chiamato implicitamente dai metodi di DbContext
– Proxy: automatico
Proxy
• Il contesto può creare dei proxy (=istanze di classi che
estendono le vostre entità) per
– supportare il lazy loading
– automatizzare «in tempo reale» (ma solo sotto opportune
condizioni)
• Change tracking
• Relationship Fix-up
• E` possibile
– istanziare i proxy tramite i metodi Create di DbSet<T>
– Disabilitarne la creazione tramite la proprietà
Configuration.ProxyCreationEnabled di DbContext
Esempio Dynamic Proxy
• Tutte le proprietà virtual
• DetectChanges() non
serve…
Esempio senza
proxy
(1/x)
• Nessuna
proprietà virtual
• In questo caso,
db.Posts.Create()
è identico a
new Post()
Esempio senza proxy (2/x)
• Add() chiama
DetectChanges()
Eager/Lazy/Explicit Loading
• Lazy = le entità collegate vengono caricate la prima
volta che vengono accedute
– È il default
– Richiede che le proprietà di siano virtual (e il lazy loading
abilitato...)
• Eager = l’esecuzione di una query per un certo tipo
entità carica anche altre entità
– Cosa caricare in più viene specificato tramite il metodo
Include
• Explicit = le entità collegate vengono caricate
esplicitamente tramite Load
http://msdn.microsoft.com/en-us/data/jj574232
Lazy loading
• Attivo di default, si può disattivare tramite:
Configuration.LazyLoadingEnabled = false;
(sul contesto)
• Funziona creando dei proxy
– Vedere anche:
http://msdn.microsoft.com/en-us/data/jj592886
– Necessita che le proprietà di navigazione (singole entità o
ICollection<>) siano virtual
• Quindi, si può «disabilitare» su singole proprietà rendendole non-virtual
using (var db = new BloggingContext()) {
var b = db.Blogs.First(); // prima SELECT...
foreach (var post in b.Posts) // seconda SELECT...
Console.WriteLine(post.Title);
}
Attenzione: dopo che il
contesto è stato Disposed
si ottiene
ObjectDisposedException...
Eager loading
using (var context = new BloggingContext()) {
// Load all blogs and related posts
var blogs1 = context.Blogs
.Include(b => b.Posts)
.ToList();
// Load one blogs and its related posts
var blog1 = context.Blogs
.Where(b => b.Name == "ADO.NET Blog")
.Include(b => b.Posts)
.FirstOrDefault();
// Load all users their related profiles, and related avatar
var users = context.Users
.Include(u => u.Profile.Avatar)
.ToList();
// Load all blogs, all related posts, and all related comments
var blogs2 = context.Blogs
.Include(b => b.Posts.Select(p => p.Comments))
.ToList();
}
Esistono overload (tendenzialmente, da evitare) che usano stringhe invece
che λ
Explicit loading
using (var context = new BloggingContext()) {
var post = context.Posts.Find(2);
// Load the blog related to a given post
context.Entry(post).Reference(p => p.Blog).Load();
var blog = context.Blogs.Find(1);
// Load the posts related to a given blog
context.Entry(blog).Collection(p => p.Posts).Load();
Si può anche usare la proprietà
IsLoaded (su Reference(...) e
Collection(...)) per vedere se è
stata precedentemente
caricata
// Load the posts with the 'entity-framework' tag related to a given blog
context.Entry(blog)
.Collection(b => b.Posts)
.Query()
.Where(p => p.Tags.Contains("entity-framework"))
.Load();
// Sometimes it is useful to know how many entities are related to another
// without actually incurring the cost of loading all those entities.
// The Query method with the LINQ Count method can be used to do this.
// Eg: Count how many posts the blog has
var postCount = context.Entry(blog)
.Collection(b => b.Posts)
.Query()
.Count();
}
Esistono overload (tendenzialmente, da evitare) che usano stringhe invece che λ
Stato Added
Un oggetto o (Untracked) diventa Added:
• Chiamando Add(o) su un DbSet
– Es: context.Blogs.Add(blog);
• Settando esplicitamente il suo stato
– Es: context.Entry(blog).State = EntityState.Added;
• «Agganciandolo» a un oggetto già attached
– Es, se blog è attached:
blog.Owner = new User { UserName = "arthur" };
blog.Posts.Add(new Post { Name = "..." });
Attach/EntityState.Modified
Un oggetto o (Untracked) si può rendere attached
tramite:
• context.Attach(o);
in questo caso il suo stato è Unchanged
• context.Entry(o).State = EntityState.Modified;
in questo caso si considera che tutte le sue
proprietà siano state modificate
Stato Deleted
• dbset.Remove(o);
– No, non si chiama Delete()
• Devo proprio caricare un’entità per cancellarla?!?!
– Sni; è la cosa più semplice, anche se non
particolarmente efficiente (!)
• In realtà, basta un oggetto con la stessa chiave (e
concurrency token) e fare Attach+Remove.
Stesso discorso vale per le modifiche in N-tier
Local & property values
• DbSet.Local
Espone le entità caricate (e non cancellate);
essendo una ObservableCollection può essere
usata direttamente per il binding in WPF
http://msdn.microsoft.com/en-us/data/jj592872
• È possibile lavorare (vedere/modificare lo stato)
delle singole proprietà
http://msdn.microsoft.com/en-us/data/jj592677
– Serve solo in scenari particolari; in generale, gli
automatismi di EF sono sufficienti
Complex Type (1/2)
• Alle volte può essere comodo creare tipi “valore” che
aggreghino delle proprietà di tipo “valore”
– Esempio, Indirizzo che aggrega Via, CAP e Città
• EF chiama questi tipi Complex types
– Non hanno chiavi (possono essere usati solo all’interno di
entità o altri CT)
– Non possono partecipare in associazioni e non hanno
proprietà di navigazione
– Non possono usare l’ereditarietà
– NB: non sono tipi valore a livello .NET, si dichiarano con
class e non struct
Complex Type (2/2)
public class Address {
public string StreetAddress { get; set; }
public string ZipCode { get; set; }
public string City { get; set; }
}
public class Order {
public int Id { get; set; }
public Address InvoiceAddress { get; set; }
public Address DeliveryAddress { get; set; }
}
CREATE TABLE [dbo].[Orders] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[InvoiceAddress_StreetAddress] NVARCHAR
[InvoiceAddress_ZipCode]
NVARCHAR
[InvoiceAddress_City]
NVARCHAR
[DeliveryAddress_StreetAddress] NVARCHAR
[DeliveryAddress_ZipCode]
NVARCHAR
[DeliveryAddress_City]
NVARCHAR
CONSTRAINT [PK_dbo.Orders] PRIMARY KEY
CLUSTERED ([Id] ASC)
);
(MAX)
(MAX)
(MAX)
(MAX)
(MAX)
(MAX)
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
Ereditarietà
public class E1 {
public int Id { get; set; }
//...
}
db.E1s.Add(new E1());
db.E1s.Add(new E2());
public class E2 : E1 {/*...*/}
E2[] e2sB =
db.E1s.OfType<E2>().ToArray();
E1[] e1s = db.E1s.ToArray();
public class BloggingContext:DbContext {
//...
public DbSet<E1> E1s { get; set; }
public DbSet<E2> E2s { get; set; }
protected override void
OnModelCreating(DbModelBuilder mb) {
mb.Entity<E1>().ToTable("E1");
mb.Entity<E2>().ToTable("E2");
}
}
E2[] e2sA = db.E2s.ToArray();
db.E2s.Add(new E2());
Ereditarietà - Mapping
Possibili mapping
• Table-Per-Hierarchy (TPH)
– Il default, purtroppo... lettura interessante:
http://sqlblog.com/blogs/merrill_aldrich/archive/2009/11/17/trick-question-part-quattro.aspx
• Table-Per-Concrete Class (TPC)
– Una schifezza (come gli è venuto in mente?!?)
• Non si possono usare campi Identity, niente relazioni nella
classe base, ...
• Table-Per-Type (TPT)
– Quello che IMHO dovrebbe essere il default
Table-Per-Type (TPT)
• Basta dare un nome tabella diverso a ogni entità
(coinvolta nella gerarchia)
– Tramite data-annotation
[Table("E1")] public class E1 {/* ... */}
[Table("E2")] public class E2 : E1 {/* ... */}
– Tramite Fluent API
mb.Entity<E1>().ToTable("E1");
mb.Entity<E2>().ToTable("E2");
Entity/Table Splitting
• È possibile anche:
– Salvare «porzioni» di entità in tabelle diverse (Entity
Splitting)
– Salvare entità diverse sul (parti de) la stessa tabella
(Table Splitting)
• Le entità devono avere una relazione 1-1 e condividere la
chiave
Validazione
• SaveChanges (indirettamente) valida le entità
(non cancellate) prima di salvare
• Il supporto per la validazione non è specifico di EF
(altre tecnologie .NET lo usano, per esempio
ASP.NET MVC)
– Di attributi, tramite classi che estendono
System.ComponentModel.DataAnnotations.Validation
Attribute
• Fra cui, CustomValidationAttribute
– Di tipi, che implementano IValidatableObject
Concorrenza
• Cosa succede se
– due (o più) programmi cercano di scrivere gli stessi dati?
• “Vince” l’ultimo?
• Si fa un “merge”?
• ...
– un programma cerca di aggiornare dati che non esistono più?
• Due approcci per gestire i conflitti:
– Concorrenza pessimistica
• Usa il locking, poco scalabile
– Concorrenza ottimistica
• Niente locking, l’applicazione deve gestire eventuali conflitti
• In EF: http://msdn.microsoft.com/en-us/data/jj592904
Concorrenza ottimistica: conflitti
• Come ci accorgiamo se ci sono dei conflitti?
• UPDATE ...
WHERE Id=... AND f1=old1 AND f2=old2 ...
– E poi si guarda quante righe sono state modificate
• 1  ok
• 0  la riga originale è stata modificata o cancellata
– EF confronta i valori dei campi che sono Concurrency
Token
• In caso di gerarchie, solo l’entità radice può averne
• Piccola miglioria: usare un campo di tipo
rowversion/timestamp
Vedere anche slide 21
«Esempio con DA e FluentAPI (1/2)»
In caso di conflitto?
• SaveChanges solleva l’eccezione
DbUpdateConcurrencyException, facendo rollback di
eventuali modifiche già avvenute
– Di default, SaveChanges usa una DbTransaction in
automatico
• L’eccezione espone la proprietà
IEnumerable<DbEntityEntry> Entries
che permette di sapere quali entità hanno creato
problemi
– Quindi, si possono ricaricare i dati dal DB, forzarne la
scrittura o optare per strategie intermedie
• Esempi: http://msdn.microsoft.com/en-us/data/jj592904
Transazioni
• Di default, non occorre preoccuparsene
• Per scenari più complessi (per esempio, che
coinvolgono più DB) è possibile:
– Recuperare gli OC tramite
IObjectContextAdapter.ObjectContext
– Wrappare il tutto in un TransactionScope
– Usare (sugli OC) SaveChanges(SaveOptions. None) o
SaveChanges(SaveOptions. DetectChangesBeforeSave)
– Solo alla fine, (sugli OC) AcceptAllChanges()
Cambio del modello/migrations
• Idea molto interessante (anche se la tecnologia è
ancora un po’ immatura...)
http://msdn.microsoft.com/en-us/data/jj591621
– Esiste anche una versione più automatica e limitata:
http://msdn.microsoft.com/en-US/data/jj554735
• In breve, dalla console del package manager
– Enable-Migrations
– Add-Migration
– Update-Database
• Le «migrazioni» rappresentate da classi C# con un
metodo Up() e un metodo Down()
Migrations: Esempio (1/3)
public class Post {
[StringLength(30)]
public string Author { get; set; }
// ... tutto il resto come prima...
Con «Add-Migration AddedAuthor» otteniamo:
public partial class AddedAuthor : DbMigration {
public override void Up() {
AddColumn("dbo.Posts", "Author",
c => c.String(maxLength: 30));
}
public override void Down() {
DropColumn("dbo.Posts", "Author");
}
}
Migrations: Esempio (2/3)
Possiamo aggiornare direttamente il DB con:
• Update-Database, o ottenere lo script per farlo
• Update-Database -Script
ALTER TABLE [dbo].[Posts] ADD [Author] [nvarchar](30)
INSERT INTO [__MigrationHistory] ([MigrationId], [Model],
[ProductVersion]) VALUES ('201211141650027_AddedAuthor',
0x1F8B080000…8DBF0190000, '5.0.0.net45')
Migrations: Esempio (3/3)
• Possiamo ottenere uno script per tornare a una
versione qualsiasi con lo switch -T; esempio:
Update-Database -T InitialCreate –Script
DECLARE @var0 nvarchar(128)
SELECT @var0 = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'dbo.Posts')
AND col_name(parent_object_id, parent_column_id) = 'Author';
IF @var0 IS NOT NULL
EXECUTE('ALTER TABLE [dbo].[Posts] DROP CONSTRAINT ' + @var0)
ALTER TABLE [dbo].[Posts] DROP COLUMN [Author]
DELETE FROM [__MigrationHistory] WHERE [MigrationId] =
'201211141650027_AddedAuthor'
DbSet<T> vs IDbSet<T>
• IDbSet<T> rappresenta un insieme di entità di tipo
T
– DbSet<T> ne é un’implementazione
• Per fare unit-testing, non possiamo usare DbSet
• Può tornare utile, per esempio, FakeDbSet<T>
http://refactorthis.wordpress.com/2011/11/30/ge
neric-repository-fake-idbset-implementationupdate-find-method-identity-key/
Unit-testing (1/2)
• Naturalmente, anche il contesto deve diventare un’interfaccia;
esempio
public interface IBloggingContext : IDisposable {
IDbSet<Blog> Blogs { get; set; }
IDbSet<Post> Posts { get; set; }
}
public class BloggingContext : DbContext, IBloggingContext {
public IDbSet<Blog> Blogs { get; set; }
public IDbSet<Post> Posts { get; set; }
}
public class InMemoryBloggingContext : IBloggingContext, IDisposable {
public InMemoryBloggingContext() {
this.Blogs = new FakeDbSet<Blog>();
this.Posts = new FakeDbSet<Post>();
}
public IDbSet<Blog> Blogs { get; set; }
public IDbSet<Post> Posts { get; set; }
public void Dispose() {}
}
Unit-testing (2/2)
public class Example {
public void InsertNewBlog(string name, IBloggingContext context) {
var blog = new Blog {Name = name};
context.Blogs.Add(blog);
}
}
[TestFixture]
public class TestExample {
[Test]
public void Example_InsertNewBlog_AddsNewBlog() {
var c = new InMemoryBloggingContext();
var target = new Example();
const string name = "foo";
target.InsertNewBlog(name, c);
Assert.That(c.Blogs.Count(b => b.Name==name)==1);
}
}
No-Tracking Queries
Quando non si vogliono modificare le entità, si può usare
AsNoTracking() per evitare che le entità vengano controllate dal
contesto
using (var context = new BloggingContext()) {
// Query for all blogs without tracking them
var blogs1 = context.Blogs.AsNoTracking();
// Query for some blogs without tracking them
var blogs2 = context.Blogs
.Where(b => b.Name.Contains(".NET"))
.AsNoTracking()
.ToList();
}
http://msdn.microsoft.com/en-us/data/jj556203
Quanti contesti? (= classi contesto)
• In scenari semplici, una
• Alle volte ha senso averne di più
– Vedere anche Bounded Context dell’approccio DDD (Domain-driven design)
– Nel caso: attenzione a «puntare» lo stesso DB e a chi lo inizializza. Può
essere utile far estendere ai contesti una classe (presa dal libro
“Programming Entity Framework: DbContext”) come:
public class BaseContext<TContext> : DbContext
where TContext : DbContext {
static BaseContext() {
Database.SetInitializer<TContext>(null);
}
protected BaseContext() : base("name=....") {}
}
public class BloggingContext : BaseContext<BloggingContext>,
IBloggingContext { //...
Quanti contesti? (=oggetti contesto)
• Il contesto è una unit-of-work
– Per ogni operazione (logica) è sensato creare un
contesto, lavorarci e poi farne Dispose
• In casi molto particolari si potrebbe mantenere un
solo contesto, ma non conviene
N-tier
In scenari N-tier se le entità vengono manipolate «lato
client» (=disconnesse dal contesto) bisogna tracciare le
modifiche e riportarle sul contesto (tipicamente, uno
nuovo...)
Possibili soluzioni
• MVVM/DTOs
(comodo: AutoMapper http://automapper.org/)
• WCF Data Services
• ASP.NET Web API
Uso (diretto) di SQL
• Tramite la proprietà Database di DbContext è
possibile gestire il DB (crearlo, cancellarlo, ...)
http://msdn.microsoft.com/en-us/library/system.data.entity.database%28v=vs.103%29.aspx
• Tramite SqlQuery/SqlCommand è possibile usare
direttamente SQL per eseguire
– Query/Stored-procedure
• Che restituiscono entità
var blogs = context.Blogs.SqlQuery(
"SELECT * FROM dbo.Blogs").ToList();
• Generiche (che non restituiscono entità)
– Comandi
context.Database.ExecuteSqlCommand(
"UPDATE dbo.Blogs SET Name = 'Another Name' WHERE Id = {0}",
2);
Riferimenti
• MSDN, Get Started with Entity Framework:
http://msdn.microsoft.com/en-us/data/ee712907
• Libri
– Programming Entity Framework (2nd ed.)
Julia Lerman
– Programming Entity Framework: Code First
Julia Lerman and Rowan Miller
– Programming Entity Framework: DbContext
Julia Lerman and Rowan Miller