Entity Framework Core e LINQ to Entities in Depth (2) Modellazione del database:mappatura relazionale degli oggetti

Entity Framework Core e LINQ to Entities in Depth (2) Modellazione del database:mappatura relazionale degli oggetti

[LINQ tramite serie C#]

[Serie Entity Framework Core (EF Core)]

[Serie Entity Framework (EF)]

In LINQ to Entities, le query si basano sul mapping relazionale a oggetti. .NET e database SQL e hanno 2 diversi sistemi di tipi di dati. Ad esempio, .NET ha System.Int64 e System.String, mentre il database SQL ha bigint e nvarchar; .NET ha sequenze e oggetti, mentre il database SQL ha tabelle e righe, ecc. La mappatura relazionale degli oggetti è una tecnologia popolare per mappare e convertire tra oggetti dati dell'applicazione e dati relazionali del database.

Tipi di dati

EF Core può mappare la maggior parte dei tipi di dati SQL ai tipi .NET:

Categoria di tipo SQL

Tipo SQL

tipo .NET

Primitiva C#

Numerico esatto

po'

System.Booleano

bollo


minuscolo

Byte di sistema

byte


piccolo

System.Int16

breve


int

System.Int32

int


grande

System.Int64

lungo


smallmoney, denaro, decimale, numerico

System.Decimal

decimale

Numerico approssimativo

reale

Sistema.Singolo

galleggiante


galleggiante

Sistema.Doppio

doppio

Stringa di caratteri

char, varchar, testo

Stringa.sistema

stringa


nchar, nvarchar, ntext

Stringa.sistema

stringa

Stringa binaria

binario, varbinario

Byte.sistema[]

byte[]


immagine

Byte.sistema[]

byte[]


rowversion (timestamp)

Byte.sistema[]

byte[]

Data e ora

data

System.DateTime



tempo

Intervallo.tempo di sistema



smalldatetime, datetime, datetime2

System.DateTime



datetimeoffset

System.DateTimeOffset


Other

hierarchyid

No built-in support



xml

System.String

string


uniqueidentifier

System.Guid



sql_variant

System.Object

object

The mapping of spatial types are supported through NetTopologySuite, a free and open source library. For SQL database, just install the Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite NuGet package.

Database

A SQL database is mapped to a type derived from Microsoft.EntityFrameworkCore.DbContext:

public partial class AdventureWorks :DbContext { }

The following is the definition of DbContext:

namespace Microsoft.EntityFrameworkCore

{

public class DbContext :IDisposable, IInfrastructure

{

public DbContext(DbContextOptions options);


public virtual ChangeTracker ChangeTracker { get; }


public virtual DatabaseFacade Database { get; }


public virtual void Dispose();


public virtual int SaveChanges();


public virtual DbSet Set() where TEntity :class;


protected internal virtual void OnModelCreating(ModelBuilder modelBuilder);


// Other members.

}

}

DbContext implements IDisposable. Generally, a database instance should be constructed and disposed for each unit of work - a collection of data operations that should succeed or fail as a unit:

internal static void Dispose()

{

using (AdventureWorks adventureWorks =new AdventureWorks())

{

// Unit of work.

}

}

EF Core also support DbContext pooling to improve performance. In the application or service, If DbContext is used through dependency injection, and it is no custom state (just like the above AdventureWorks type with no fields), then DbContext pooling can be enabled to reuse DbContext without disposing.

In EF Core, most of the object-relational mapping can be implemented declaratively, and the rest of the mapping can be implemented imperatively by overriding DbContext.OnModelCreating, which is automatically called by EF Core during initialization:

public partial class AdventureWorks

{

protected override void OnModelCreating(ModelBuilder modelBuilder)

{

base.OnModelCreating(modelBuilder);

MapCompositePrimaryKey(modelBuilder);

MapManyToMany(modelBuilder);

MapDiscriminator(modelBuilder);

}

}

The above MapCompositePrimaryKey, MapManyToMany, MapDiscriminator functions are implemented later in this chapter.

Connection resiliency and execution retry strategy

The connection to the mapped database can be specified from the constructor of DbContext:

public partial class AdventureWorks

{

public AdventureWorks(DbConnection connection =null)

:base(GetDbContextOptions(connection))

{

}


private static DbContextOptions GetDbContextOptions(

DbConnection connection =null) =>

new DbContextOptionsBuilder()

.UseSqlServer(

connection:connection ??

new SqlConnection(ConnectionStrings.AdventureWorks),

sqlServerOptionsAction:options => options.EnableRetryOnFailure(

maxRetryCount:5, maxRetryDelay:TimeSpan.FromSeconds(30),

errorNumbersToAdd:null))

.Options;

}

Here when database connection is not provided to the constructor, a new database connection is created with the previously defined connection string. Also, regarding the connection between application and SQL database may be interrupted over the network, EF Core supports connection resiliency for SQL database. This is very helpful for Azure SQL database deployed in the cloud instead of local network. In the above example, EF Core is specified to automatically retries up to 5 times with the retry interval of 30 seconds.

Tables

There are tens of tables in the AdventureWorks database, but don’t panic, this book only involves a few tables, and a few columns of these tables. In EF Core, a table definition can be mapped to an entity type definition, where each column is mapped to a entity property. For example, the AdventureWorks database has a Production.ProductCategory table. Its definition can be virtually viewed as:

CREATE SCHEMA [Production];

GO

CREATE TYPE [dbo].[Name] FROM nvarchar(50) NULL;

GO

CREATE TABLE [Production].[ProductCategory](

[ProductCategoryID] int IDENTITY(1,1) NOT NULL

CONSTRAINT [PK_ProductCategory_ProductCategoryID] PRIMARY KEY CLUSTERED,

[Name] [dbo].[Name] NOT NULL, -- nvarchar(50).

[rowguid] uniqueidentifier ROWGUIDCOL NOT NULL -- Ignored in mapping.

CONSTRAINT [DF_ProductCategory_rowguid] DEFAULT (NEWID()),

[ModifiedDate] datetime NOT NULL -- Ignored in mapping.

CONSTRAINT [DF_ProductCategory_ModifiedDate] DEFAULT (GETDATE()));

GO

This table definition can be mapped to a ProductCategory entity definition:

public partial class AdventureWorks

{

public const string Production =nameof(Production); // Production schema.

}

[Table(nameof(ProductCategory), Schema =AdventureWorks.Production)]

public partial class ProductCategory

{

[Key]

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]

public int ProductCategoryID { get; set; }

[MaxLength(50)]

[Required]

public string Name { get; set; }

// Other columns are ignored.

}

The [Table] attribute specifies the table name and schema. [Table] can be omitted when the table name is the same as the entity name, and the table is under the default dbo schema. In the table-entity mapping:

· The ProductCategoryID column of int type is mapped to a System.Int32 property with the same name. The [Key] attribute indicates it is a primary key. EF Core requires a table to have primary key to be mapped. [DatabaseGenerated] indicates it is an identity column, with value generated by database.

· The Name column is of dbo.Name type. which is actually nvarchar(50), so it is mapped to Name property of type System.String. The [MaxLength] attribute indicates the max length of the string value is 50. [Required] indicates it should not be null or empty string or whitespace string.

· The other columns rowguid and ModifiedDate are not mapped. They are not used in this book to keep the code examples simple.

At runtime, each row of Production.ProductCategory table is mapped to a ProductCategory entity instance. The entire table can be mapped to an IQueryable data source, exposed as a property of the database mapping. EF Core provides DbSet, which implements IQueryable, to represent a table data source:

public partial class AdventureWorks

{

public DbSetProductCategories { get; set; }

}

EF Core also supports immutable entity definition:

[Table(nameof(ProductCategory), Schema =AdventureWorks.Production)]

public partial class ProductCategory

{

public ProductCategory(int productCategoryID, string name) =>

(this.ProductCategoryID, this.Name) =(productCategoryID, name);


[Key]

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]

public int ProductCategoryID { get; private set; }


[MaxLength(50)]

[Required]

public string Name { get; private set; }

}

This book defines all table mapping as mutable, since it is easier to update the entities and save back to database.

Relationships

In SQL database, tables can have foreign key relationships, including one-to-one, one-to-many, and many-to-many relationships.

One-to-one

The following Person.Person table and HumanResources.Employee table has a one-to-one relationship:

HumanResources.Employee table’s BusinessEntityID column is a foreign key that refers to Person.Person table’s primary key. Their definition can be virtually viewed as:

CREATE TABLE [Person].[Person](

[BusinessEntityID] int NOT NULL

CONSTRAINT [PK_Person_BusinessEntityID] PRIMARY KEY CLUSTERED,


[FirstName] [dbo].[Name] NOT NULL,


[LastName] [dbo].[Name] NOT NULL


/* Other columns. */);

GO

CREATE TABLE [HumanResources].[Employee](

[BusinessEntityID] int NOT NULL

CONSTRAINT [PK_Employee_BusinessEntityID] PRIMARY KEY CLUSTERED

CONSTRAINT [FK_Employee_Person_BusinessEntityID] FOREIGN KEY

REFERENCES [Person].[Person] ([BusinessEntityID]),

[JobTitle] nvarchar(50) NOT NULL,


[HireDate] date NOT NULL


/* Other columns. */);

GO

So each row in HumanResources.Employee table refers to one row in Person.Person table (an employee must be a person). On the other hand, each row in Person.Person table can be referred by 0 or 1 row in HumanResources.Employee table (a person can be an employee, or not). This relationship can be represented by navigation property of entity type:

public partial class AdventureWorks

{

public const string Person =nameof(Person);

public const string HumanResources =nameof(HumanResources);

public DbSet People { get; set; }

public DbSet Employees { get; set; }

}

[Table(nameof(Person), Schema =AdventureWorks.Person)]

public partial class Person

{

[Key]

public int BusinessEntityID { get; set; }

[Required]

[MaxLength(50)]

public string FirstName { get; set; }

[Required]

[MaxLength(50)]

public string LastName { get; set; }

public virtual Employee Employee { get; set; } // Reference navigation property.

}

[Table(nameof(Employee), Schema =AdventureWorks.HumanResources)]

public partial class Employee

{

[Key]

[ForeignKey(nameof(Person))]

public int BusinessEntityID { get; set; }

[Required]

[MaxLength(50)]

public string JobTitle { get; set; }

public DateTime HireDate { get; set; }

public virtual Person Person { get; set; } // Reference navigation property.

}

The [ForeignKey] attribute indicates Employee entity’s BusinessEntityID property is the foreign key for the relationship represented by navigation property. Here Person is called the primary entity, and Employee is called the dependent entity. Their navigation properties are called reference navigation properties, because each navigation property can refer to a single entity. The navigation property is designed to be virtual to enable proxy entity to implement lazy loading. Proxy entity and lazy loading is discussed in the query translation and data loading chapter.

One-to-many

The Production.ProductCategory and Production.ProductSubcategory tables have a one-to-many relationship, so are Production.ProductSubcategory and Production.Product:

Each row in Production.ProductCategory table can refer to many rows in Production.ProductSubcategory table (category can have many subcategories), and each row in Production.ProductSubcategory table can refer to many rows in Production.Product table (subcategory can have many products). Their definitions can be virtually viewed as:

CREATE TABLE [Production].[ProductSubcategory](

[ProductSubcategoryID] int IDENTITY(1,1) NOT NULL

CONSTRAINT [PK_ProductSubcategory_ProductSubcategoryID] PRIMARY KEY CLUSTERED,


[Name] [dbo].[Name] NOT NULL, -- nvarchar(50).

[ProductCategoryID] int NOT NULL

CONSTRAINT [FK_ProductSubcategory_ProductCategory_ProductCategoryID] FOREIGN KEY

REFERENCES [Production].[ProductCategory] ([ProductCategoryID]),


/* Other columns. */)

GO

CREATE TABLE [Production].[Product](

[ProductID] int IDENTITY(1,1) NOT NULL

CONSTRAINT [PK_Product_ProductID] PRIMARY KEY CLUSTERED,


[Name] [dbo].[Name] NOT NULL, -- nvarchar(50).

[ListPrice] money NOT NULL,


[ProductSubcategoryID] int NULL

CONSTRAINT [FK_Product_ProductSubcategory_ProductSubcategoryID] FOREIGN KEY

REFERENCES [Production].[ProductSubcategory] ([ProductSubcategoryID])

/* Other columns. */)

GO

These one-to-many relationships can be represented by navigation property of type ICollection:

public partial class ProductCategory

{

public virtual ICollection ProductSubcategories { get; set; } // Collection navigation property.

}

[Table(nameof(ProductSubcategory), Schema =AdventureWorks.Production)]

public partial class ProductSubcategory

{

[Key]

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]

public int ProductSubcategoryID { get; set; }

[MaxLength(50)]

[Required]

public string Name { get; set; }

public int ProductCategoryID { get; set; }

public virtual ProductCategory ProductCategory { get; set; } // Reference navigation property.

public virtual ICollection Products { get; set; } // Collection navigation property.

}

[Table(nameof(Product), Schema =AdventureWorks.Production)]

public partial class Product

{

[Key]

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]

public int ProductID { get; set; }

[MaxLength(50)]

[Required]

public string Name { get; set; }

public decimal ListPrice { get; set; }

public int? ProductSubcategoryID { get; set; }

public virtual ProductSubcategory ProductSubcategory { get; set; } // Reference navigation property.

}

Notice Production.Product table’s ProductSubcategoryID column is nullable, so it is mapped to a int? property. Here [ForeignKey] attribute is omitted, because each dependent entity’ foreign key is separated from its primary key, so the foreign key can be automatically discovered by EF Core.

Many-to-many

Production.Product and Production.ProductPhoto tables has many-to-many relationship.

This is implemented by 2 one-to-many relationships with another Production.ProductProductPhoto junction table. These tables’ definitions can be virtually viewed as:

CREATE TABLE [Production].[ProductPhoto](

[ProductPhotoID] int IDENTITY(1,1) NOT NULL

CONSTRAINT [PK_ProductPhoto_ProductPhotoID] PRIMARY KEY CLUSTERED,


[LargePhotoFileName] nvarchar(50) NULL,

[ModifiedDate] datetime NOT NULL

CONSTRAINT [DF_ProductPhoto_ModifiedDate] DEFAULT (GETDATE())


/* Other columns. */)

GO

CREATE TABLE [Production].[ProductProductPhoto](

[ProductID] int NOT NULL

CONSTRAINT [FK_ProductProductPhoto_Product_ProductID] FOREIGN KEY

REFERENCES [Production].[Product] ([ProductID]),


[ProductPhotoID] int NOT NULL

CONSTRAINT [FK_ProductProductPhoto_ProductPhoto_ProductPhotoID] FOREIGN KEY

REFERENCES [Production].[ProductPhoto] ([ProductPhotoID]),


CONSTRAINT [PK_ProductProductPhoto_ProductID_ProductPhotoID] PRIMARY KEY NONCLUSTERED ([ProductID], [ProductPhotoID])

/* Other columns. */)

GO

So the many-to-many relationship can be mapped to 2 one-to-many relationships with the junction:

public partial class Product

{

public virtual ICollection ProductProductPhotos { get; set; } // Collection navigation property.

}

[Table(nameof(ProductPhoto), Schema =AdventureWorks.Production)]

public partial class ProductPhoto

{

[Key]

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]

public int ProductPhotoID { get; set; }

[MaxLength(50)]

public string LargePhotoFileName { get; set; }

[ConcurrencyCheck]

public DateTime ModifiedDate { get; set; }

public virtual ICollection ProductProductPhotos { get; set; } // Collection navigation property.

}

[Table(nameof(ProductProductPhoto), Schema =AdventureWorks.Production)]

public partial class ProductProductPhoto

{

[Key]

[Column(Order =0)]

public int ProductID { get; set; }

[Key]

[Column(Order =1)]

public int ProductPhotoID { get; set; }

public virtual Product Product { get; set; } // Reference navigation property.

public virtual ProductPhoto ProductPhoto { get; set; } // Reference navigation property.

}

ProductPhoto.ModifiedDate has a [ConcurrencyCheck] attribute for concurrency conflict check, which is discussed in the data manipulation chapter. Production.ProductProductPhoto table has a composite primary key. As a junction table, each row in the table has a unique combination of ProductID and ProductPhotoID. EF Core requires additional initialization for composite primary key, which should be executed in DbContext’s OnModelCreating method:

public partial class AdventureWorks

{

private static void MapCompositePrimaryKey(ModelBuilder modelBuilder) // Called by OnModelCreating.

{

modelBuilder.Entity()

.HasKey(productProductPhoto => new

{

ProductID =productProductPhoto.ProductID,

ProductPhotoID =productProductPhoto.ProductPhotoID

});

}

}

EF Core also requires additional initialization for many-to-many relationship represented by 2 one-to-many relationships, which should be executed in OnModelCreating as well:

public partial class AdventureWorks

{

private static void MapManyToMany(ModelBuilder modelBuilder) // Called by OnModelCreating.

{

modelBuilder.Entity()

.HasOne(productProductPhoto => productProductPhoto.Product)

.WithMany(product => product.ProductProductPhotos)

.HasForeignKey(productProductPhoto => productProductPhoto.ProductID);

modelBuilder.Entity()

.HasOne(productProductPhoto => productProductPhoto.ProductPhoto)

.WithMany(photo => photo.ProductProductPhotos)

.HasForeignKey(productProductPhoto => productProductPhoto.ProductPhotoID);

}

}

Finally, all the above tables can be exposed as properties of AdventureWorks:

public partial class AdventureWorks

{

public DbSet People { get; set; }

public DbSet Employees { get; set; }

public DbSetProductSubcategories { get; set; }

public DbSet Products { get; set; }

public DbSetProductPhotos { get; set; }

}

Inheritance

EF Core supports table per hierarchy (TPH) inheritance for entity types. With TPH, one table is mapped to many entity types in the inheritance hierarchy, so a discriminator column is needed to identify each specific row’s mapping entity. Take the Production.TransactionHistory table as example, its definition can be virtually viewed as:

CREATE TABLE [Production].[TransactionHistory](

[TransactionID] int IDENTITY(100000,1) NOT NULL

CONSTRAINT [PK_TransactionHistory_TransactionID] PRIMARY KEY CLUSTERED,


[ProductID] int NOT NULL

CONSTRAINT [FK_TransactionHistory_Product_ProductID] FOREIGN KEY

REFERENCES [Production].[Product] ([ProductID]),


[TransactionDate] datetime NOT NULL,


[TransactionType] nchar(1) NOT NULL

CONSTRAINT [CK_Product_Style]

CHECK (UPPER([TransactionType]) =N'P' OR UPPER([TransactionType]) =N'S' OR UPPER([TransactionType]) =N'W'),


[Quantity] int NOT NULL,


[ActualCost] money NOT NULL


/* Other columns. */);

GO

Its TransactionType column allows value “P”, “S”, or “W” to indicate each row representing a purchase transaction, sales transaction, or work transaction. So the mapping entities’ hierarchy can be:

[Table(nameof(TransactionHistory), Schema =AdventureWorks.Production)]

public abstract class TransactionHistory

{

[Key]

public int TransactionID { get; set; }


public int ProductID { get; set; }


public DateTime TransactionDate { get; set; }


public int Quantity { get; set; }


public decimal ActualCost { get; set; }

}


public class PurchaseTransactionHistory :TransactionHistory { }


public class SalesTransactionHistory :TransactionHistory { }


public class WorkTransactionHistory :TransactionHistory { }

Then the discriminator must be specified when OnModelCreating is executed:

public enum TransactionType { P, S, W }


public partial class AdventureWorks

{

private static void MapDiscriminator(ModelBuilder modelBuilder) // Called by OnModelCreating.

{

modelBuilder.Entity()

.HasDiscriminator(nameof(TransactionType))

.HasValue(nameof(TransactionType.P))

.HasValue(nameof(TransactionType.S))

.HasValue(nameof(TransactionType.W));

}

}

Now these entities can all be exposed as data sources:

public partial class AdventureWorks

{

public DbSetTransactions { get; set; }

public DbSet PurchaseTransactions { get; set; }

public DbSetSalesTransactions { get; set; }

public DbSetWorkTransactions { get; set; }

}

Views

A view can also be mapped as if it is a table, if the view has one or more columns which can be virtually viewed as primary key. Take the Production.vEmployee view as example, its definition can be virtually viewed as:

CREATE VIEW [HumanResources].[vEmployee]

AS

SELECT

e.[BusinessEntityID],

p.[FirstName],

p.[LastName],

e.[JobTitle]

-- Other columns.

FROM [HumanResources].[Employee] e

INNER JOIN [Person].[Person] p

ON p.[BusinessEntityID] =e.[BusinessEntityID]

/* Other tables. */;

GO

The BusinessEntityID is unique and can be the virtual primary key. So it can be mapped as the following entity:

[Table(nameof(vEmployee), Schema =AdventureWorks.HumanResources)]

public class vEmployee

{

[Key]

public int BusinessEntityID { get; set; }


public string FirstName { get; set; }


public string LastName { get; set; }


public string JobTitle { get; set; }

}

And then expose as data source:

public partial class AdventureWorks

{

public DbSetvEmployees { get; set; }

}

Summary

Text: