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= &quot;Data Source=.\sqlexpress; Initial Catalog=Northwind; Integrated Security=True; MultipleActiveResultSets=True&quot;" 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: DBC# 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