Entity Framework 5.0 e Microsoft SQL Server (parte 1)

Oggi esploreremo il comportamento delle EntityFramework 5.0 su SQL Server, in particolare il comportamento che si ha utilizzando l’approccio Code First. Per questo esempio creiamo un progetto console con Visual Studio e aggiungiamo (tramite NUGet ad esempio) i riferimenti ad EntityFramework:

 

Ora aggiungiamo due entità (classi), una chiamata Crewman:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication3
{
    public class Crewman
    {
        public int ID { get; set; }

        public string Name { get; set; }
        public Rank Rank { get; set; }

        public Crewman ReportingOfficier { get; set; }
    }
}

 E l’altra chiamata StarShip:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.Entity;

namespace ConsoleApplication3
{
    public class Starship
    {
        public Starship()
        {
            Crew = new List<Crewman>();
        }

        public int ID { get; set; }
        
        public string Name { get; set; }
        public string Designation { get; set; }

        public List<Crewman> Crew { get; set; }
    }
}

 Il tipo Rank è un enumerato così definito:

 public enum Rank
    {
        Captain, FirstOfficer, ChiefEngineer, Crewman, Medic
    }

 

Notiamo come abbiamo deliberatamente sempre specificato una proprietà pubblica di tipo int con nome ID. Inoltre notiamo come abbiamo creato una relazione fra le entità semplicemente definendo una proprietà come enumerabile di Crewman. Nel modello relazionale potremmo dire che l’entità Starship ha 0..∞ Crewman. Inoltre possiamo dire che ciascun Crewman può avere un Crewman ReportingOfficier.

Per rendere persistenti le entità definiamo una estensione di DbContext in questo modo:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.Entity;

namespace ConsoleApplication3
{    
    public class StarFleet : DbContext
    {
        public DbSet<Starship> StarShips { get; set; }
        public DbSet<Crewman> Crew { get; set; }

        public StarFleet(string nameOrConnectionString)
            : base(nameOrConnectionString)
        { }
    }
}

 Notiamo l’utilizzo dell’insieme DbSet per le proprietà pubbliche da persistere su SQL Server. A questo punto l’utilizzo è semplice, nel metodo main della nostra classe console creiamo una istanza di StarFleet (con il puntamento ad una nostra istanza di SQL) e popoliamola con dati fittizi:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                StarFleet sf = new StarFleet("Server=”ServerHere”;Database=TestEFCF;Trusted_Connection=True");

                {
                    Crewman cKirk = new Crewman() { Name = "J.T. Kirk", Rank = ConsoleApplication3.Rank.Captain };
                    Crewman cSpock = new Crewman() { Name = "Spock", Rank = ConsoleApplication3.Rank.FirstOfficer, ReportingOfficier = cKirk };
                    Crewman cBones = new Crewman() { Name = "Leonard McCoy", Rank = ConsoleApplication3.Rank.Medic, ReportingOfficier = cKirk };

                    Starship s = new Starship() { Name = "USS Enterprise A" };
                    s.Crew.Add(cKirk);
                    s.Crew.Add(cSpock);
                    s.Crew.Add(cBones);

                    sf.StarShips.Add(s);
                }
                //-----------------------------
                {
                    Crewman cPicard = new Crewman() { Name = "Jean Luc Picard", Rank = ConsoleApplication3.Rank.Captain };
                    Crewman cData = new Crewman() { Name = "Data", Rank = ConsoleApplication3.Rank.FirstOfficer, ReportingOfficier = cPicard };
                    Crewman cCrusher = new Crewman() { Name = "Wesley Crusher", Rank = ConsoleApplication3.Rank.Captain, ReportingOfficier = cData };
                    Starship s2 = new Starship() { Name = "USS Enterprise C" };
                    s2.Crew.Add(cPicard);
                    s2.Crew.Add(cData);
                    s2.Crew.Add(cCrusher);

                    sf.StarShips.Add(s2);
                }
                //------------------------------
                sf.SaveChanges();

            }
            catch (Exception exce)
            {
                Console.WriteLine(exce.ToString());
            }
        }
    }
}

Notiamo subito che il Database non è necessario che esista: in caso negativo il framework cercherà di crearne uno ex-novo (ovviamente la login deve possedere i relativi privilegi perché questa creazione vada a buon fine). Eseguendo il programma otterremo un database con le seguenti tabelle:

Ma cosa succeed dietro le quinte? Attiviamo una sessione di eXtended Events (non più profiler :)) usando il template “Query detail tacking”:

 

Rieseguiamo il programma (supponendo di aver ripulito il db) e vedremo che i passi effettuati dall’entity framework (tralasciamo la tabella [dbo].[__MigrationHistory] che è privata del framework e non ci interessa in questo contesto).

Vedendo che non esistono le tabelle richieste, il framework le crea per noi:

 CREATE TABLE [dbo].[Starships] (      
    [ID] [int] NOT NULL IDENTITY,      
    [Name] [nvarchar](max),      
    [Designation] [nvarchar](max),      
    CONSTRAINT [PK_dbo.Starships] PRIMARY KEY ([ID])  
    ) 

CREATE TABLE [dbo].[Crewmen] (      
    [ID] [int] NOT NULL IDENTITY,      
    [Name] [nvarchar](max),      
    [Rank] [int] NOT NULL,      
    [ReportingOfficier_ID] [int],      
    [Starship_ID] [int],      
    CONSTRAINT [PK_dbo.Crewmen] PRIMARY KEY ([ID])
)

 In seguito vengono creati gli indici:

 CREATE INDEX [IX_ReportingOfficier_ID] ON [dbo].[Crewmen]([ReportingOfficier_ID]) 

CREATE INDEX [IX_Starship_ID] ON [dbo].[Crewmen]([Starship_ID])

 E poi le constraint:

 ALTER TABLE [dbo].[Crewmen] 
ADD CONSTRAINT [FK_dbo.Crewmen_dbo.Crewmen_ReportingOfficier_ID] 
FOREIGN KEY ([ReportingOfficier_ID]) REFERENCES [dbo].[Crewmen] ([ID]) 


ALTER TABLE [dbo].[Crewmen] 
ADD CONSTRAINT [FK_dbo.Crewmen_dbo.Starships_Starship_ID] 
FOREIGN KEY ([Starship_ID]) REFERENCES [dbo].[Starships] ([ID])

Notiamo subito un paio cose interssanti:

  1. L’ordine di creazione delle tabelle è quello relazionale e non quello del codice.
  2. L’entity framework crea per noi gli indici sulla chiave esterna.

Mentre la prima è attesa, la seconda è una ottima notizia: utilizzando SSMS 2012 avremmo avuto solo la constraint e non l’indice:

 /* To prevent any potential data loss issues, you should review this script in detail before running it outside the context of the database designer.*/
BEGIN TRANSACTION
SET QUOTED_IDENTIFIER ON
SET ARITHABORT ON
SET NUMERIC_ROUNDABORT OFF
SET CONCAT_NULL_YIELDS_NULL ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
COMMIT
BEGIN TRANSACTION
GO
ALTER TABLE dbo.Starships SET (LOCK_ESCALATION = TABLE)
GO
COMMIT
BEGIN TRANSACTION
GO
ALTER TABLE dbo.Crewmen ADD CONSTRAINT
    FK_Crewmen_Starships FOREIGN KEY
    (
    Starship_ID
    ) REFERENCES dbo.Starships
    (
    ID
    ) ON UPDATE  NO ACTION 
     ON DELETE  NO ACTION 
    
GO
ALTER TABLE dbo.Crewmen SET (LOCK_ESCALATION = TABLE)
GO
COMMIT

Gli inserimenti avvengono in questa maniera:

 exec sp_executesql N'insert [dbo].[Crewmen]([Name], [Rank], [ReportingOfficier_ID], [Starship_ID])
values (@0, @1, @2, @3)

select [ID]
from [dbo].[Crewmen]
where @@ROWCOUNT > 0 and [ID] = scope_identity()',

N'@0 nvarchar(max) ,@1 int,@2 int,@3 int',
@0=N'Leonard McCoy',
@1=4,
@2=3,
@3=1

Notiamo come ogni inserimento siano in realtà due statement separati:

 insert [dbo].[Crewmen]([Name], [Rank], [ReportingOfficier_ID], [Starship_ID])
values (@0, @1, @2, @3)

select [ID]  from [dbo].[Crewmen]  where @@ROWCOUNT > 0 and [ID] = scope_identity()

(NB: potreste vedere comparire anche un SELECT StatMan([SC0])… ma questo non è dovuto ovviamente a EF).

Evidentemente EF usa il secondo statement per sincronizzare l’entità persisita via codice con l’ID generato da SQL. Domanda: come mai l’EF non usa la clausola OUTPUT?

La risposta più probabile è che si aspetti zero record in caso di fallimento (da where @@ROWCOUNT > 0) mentre lo statement OUTPUT restituisce righe anche se l’operazione fallisce. Per maggiori dettagli vedere http://msdn.microsoft.com/en-us/library/ms177564(v=sql.110).aspx.

Nei post successivi continueremo con l’analisi delle interazioni EF5.0-SQL.

 

Happy coding,

Francesco Cogno