第一个例子使用了一个Book类型,把这种类型映射到SQL Server数据库中的Books表。把记录写到数据库,然后读取、更新和删除它们。
在第一个示例中,首先创建数据库或者从应用程序创建数据库。为了先创建数据库,可以使用Visual Studio 2017中的SQL Server Object Explorer。选择数据库实例(localdb)\MSSQLLocalDB(随Visual Studio一起安装),单击树视图中的Databases节点,然后选择Add New Database。示例数据库WroxBooks只有一个表Books。
为了创建Books表,可以在WroxBooks数据库中选择Tables节点,然后选择Add New Table。使用如图所示的设计器,或者在T-SQL编辑器中输入SQL DDL语句,就可以创建Books表。下面的代码片段显示了创建表的T-SQL代码。单击Update按钮,可以将更改提交到数据库。
CREATE TABLE [dbo].[Table] ( [BookId] INT IDENTITY (1, 1) NOT NULL, [Title] NVARCHAR (50) NOT NULL, [Publisher] NVARCHAR (25) NULL, CONSTRAINT [PK_Books] PRIMARY KEY CLUSTERED ([BookId] ASC) );本章使用的示例应用程序都是.NET Core控制台应用程序,使用以下依赖项和名称空间:
依赖项
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFramework.Design
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Logging.Console
名称空间
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.ChangeTracking
Microsoft.EntityFrameworkCore.Diagnostics
Microsoft.EntityFrameworkCore.Infrastructure
Microsoft.EntityFrameworkCore.Metadata.Builders
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Logging
System
System.Collection.Generic
System.ComponentModel.DataAnnotations
System.ComponentModel.DataAnnotations.Schema
System.Linq
System.Threading.Tasks
1. 创建模型
访问Books表的BookSamples示例应用程序是一个.NET Core控制台应用程序。在这个应用程序中,Book类是一个简单的实体类型,定义了三个属性。BookId属性映射到表的主键,Title属性映射到Title列,Publisher属性映射到Publisher列。对于Title属性,应用Required属性是因为映射列在数据库中定义为NOT NULL。使用StringLength属性应用Title和Publisher属性的长度。这也映射到数据库中的列。为了把类型映射到Books表,将Table特性应用于类型:
[Table("Books")] public class Book { public int BookId { get; set; } [Required] [StringLength(50)] public string Title { get; set; } [StringLength(25)] public string Publisher { get; set; } }2. 约定、注释和流利API
EF Core使用了三个概念来定义模型:约定、注释和流利API。按照约定,有些事情会自动发生。例如,用Id前缀命名int或Guid类型的属性,将该属性映射到主键。
可以使用注释重写约定——指定特性。前面的例子使用Table特性将Book类型映射到Books表。还有一个映射到表格的约定:使用上下文的属性名。下一节将展示如何创建上下文。并不是每个约定都有注释。还使用了Required和StringLength特性。注释比约定更强大;可以做得更多。
除了使用注释,还可以使用流利API,这意味着配置是通过代码完成的,而不是使用特性完成的。在流利API中,可以使用方法的返回值来调用下一个方法。用于EF Core的流利API比注释更强大;可以做得更多。
3. 创建上下文
通过创建BooksContext类,实现了Book表与数据库的关系。这个类派生自基类DbContext。BooksContext类定义了DbSet<Book>类型的Books属性。这个类型允许创建查询,添加Book实例,存储在数据库中。要定义连接字符串,可以重写DbContext的OnConfiguring方法。在这里,UseSqlServer扩展方法将上下文映射到SQL Server数据库:
public class BooksContext: DbContext { private const string ConnectionString = @"server=(localdb)\MSSQLLocalDb;database=WroxBooks"+ "trusted_connection=true"; public DbSet<Book> Books { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlServer(ConnectionString); } }注意:
定义连接字符串的另一种选择是使用依赖注入,参见本章后面的内容。
4. 创建数据库
前面定义了模型和上下文类。现在还可以以编程方式创建数据库。首先实例化BooksContext对象。using语句确保在using作用域结束时关闭数据库连接。
使用DbContext的Database属性时,会返回一个DatabaseFacade。可以使用它创建和删除数据库,并直接发送SQL语句。调用EnsureCreatedAsync方法,确保创建了数据库。如果数据库已经存在,此方法将返回false。如果数据库不存在,则根据上下文和模型的定义创建数据库和表,并返回true:
private static async Task CreateTheDatabaseAsync() { using (var context = new BooksContext()) { bool created = await context.Database.EnsureCreatedAsync(); string creationInfo = created ?"created":"exists"; Console.WriteLine($"database {creationInfo}"); } }运行这个程序时,如果之前已经创建了这个数据库,那么字符串database exists就会写入控制台。如果之前没有创建该数据库,就创建数据库,然后写入字符串database created。
注意:
许多代码示例都使用了EF Core的异步方法,如EnsureCreatedAsync和SaveChangesAsync。如果不需要异步功能(例如,在控制台应用程序或Web应用程序中),则可以使用这些方法的同步变体。尽管异步有一些开销,但是这些API的同步版本会阻塞调用线程。EnsureCreated和SaveChanges是同步API,而EnsureCreatedAsync和SaveChangesAsync是异步API。
注意:
使用上下文方法的异步变体允许在后台启动操作。但是,不能在同一上下文中并行地启动多个操作。在开始下一个操作之前,需要等待操作完成。
5. 删除数据库
数据库的删除与它的创建非常的类似。只需要调用DatabaseFacade的方法EnsureDeletedAsync:
private static async Task DeleteDatabaseAsync() { Console.WriteLine("Delete the database,y/n?"); string input = Console.ReadLine(); if (input.ToLower() == "y") { using (var context = new BooksContext()) { bool deleted = await context.Database.EnsureDeletedAsync(); string deletionInfo = deleted ?"deleted":"not deleted"; Console.WriteLine(deletionInfo); } } }确保删除不应该删除的数据库。注意所使用的连接字符串。
6. 写入数据库
创建了数据库和Books表后,就可以用数据填充表了。创建AddBookAsync方法,把Book对象添加到数据库中。AddBookAsync方法仅把Book对象添加到上下文中,不写入数据库。必须调用SaveChangesAsync方法把Book对象写入数据库:
private static async Task AddBookAsync(string title,string publisher) { using (var context = new BooksContext()) { var book = new Book { Title = title, Publisher = publisher }; await context.Books.AddAsync(book); int records = await context.SaveChangesAsync(); Console.WriteLine($"{records} record added"); } }为了添加一组图书,可以使用AddRange方法:
private static async Task AddBooksAsync() { using (var context = new BooksContext()) { Book[] books = { new Book { Title = "Professional C# 6 and .NET Core 1.0", Publisher = "Wrox Press" }, new Book { Title = "Professional C# 5 and .NET 4.5.1", Publisher = "Wrox Press" }, new Book { Title = "JavaScript for Kids", Publisher = "Wrox Press" }, new Book { Title = "Web Design wiht HTML and CSS", Publisher = "For Dummies" } }; await context.AddRangeAsync(books); int records = await context.SaveChangesAsync(); Console.WriteLine($"{records} records added"); } }运行应用程序,调用这些方法,就可以使用SQL Server Object Explorer查看写入数据库的数据。
7. 读取数据库
为了在C#代码中读取数据,只需要调用BooksContext,访问Books属性。访问该属性会创建一个SQL语句,从数据库中检索所有的书:
private static async Task ReadBooksAsync() { using (var context = new BooksContext()) { List<Book> books = await context.Books.ToListAsync(); foreach (var book in books) { Console.WriteLine($"{book.BookId,-10} {book.Title,-40} {book.Publisher,-20}"); } } }输出结果:
1 Professional C# 6 and .NET Core 1.0 Wrox Press 2 Professional C# 5 and .NET 4.5.1 Wrox Press 3 JavaScript for Kids Wrox Press 4 Web Design wiht HTML and CSS For Dummies在调试期间打开IntelliTrace Events窗口,就可以看到发送到数据库的SQL语句(这需要Visual Studio 企业版):
SELECT [b].[Publisher], [b].[Title] FROM [Books] As [b]Entity Framework提供了一个LINQ提供程序。使用它可以创建LINQ查询来访问数据库。也可以使用方法语法,如下所示:
private static async Task QueryBooksAsync() { using (var context = new BooksContext()) { List<Book> wroxBooks = await context.Books .Where(b => b.Publisher == "Wrox Press") .ToListAsync(); //var wroxBooks = await (from b in context.Books // where b.Publisher == "Wrox Press" // select b) // .ToListAsync(); foreach (var book in wroxBooks) { Console.WriteLine($"{book.BookId,-10} {book.Title,-40} {book.Publisher,-20}"); } } }或使用声明性的LINQ查询语法:
var wroxBooks = await (from b in context.Books where b.Publisher == "Wrox Press" select b) .ToListAsync();使用两个语法变体,将这个SQL语法发送到数据库:
SELECT [b].[BookId], [b].[Publisher], [b].[Title] FROM [Books] AS [b] WHERE [b].[Publisher] = N'Wrox Press'运行结果:
1 Professional C# 6 and .NET Core 1.0 Wrox Press 2 Professional C# 5 and .NET 4.5.1 Wrox Press 3 JavaScript for Kids Wrox Press8. 更新记录
更新记录很容易实现:修改用上下文加载的对象,并调用SaveChangesAsync:
private static async Task UpdateBookAsync() { using (var context = new BooksContext()) { int records = 0; var book = await context.Books .Where(b => b.Title == "Professional C# 7") .FirstOrDefaultAsync(); if (book != null) { book.Title = "Professional C# 7 and .NET Core 2.0"; records = await context.SaveChangesAsync(); } Console.WriteLine($"{records} record updated"); } }9. 删除记录
最后,清理数据库,删除所有记录。为此,可以检索所有记录,并调用Remove或RemoveRange方法,把上下文中对象的状态设置为删除。现在调用SaveChangesAsync方法,从数据库中删除记录,并为每一个对象调用SQL Delete语句:
private static async Task DeleteBooksAsync() { using (var context = new BooksContext()) { var books = context.Books; context.Books.RemoveRange(books); int records = await context.SaveChangesAsync(); Console.WriteLine($"{records} records deleted"); } }删除一条记录:
private static async Task DeleteSingleBookAsync() { using (var context = new BooksContext()) { var book = context.Books.Where(b => b.Title == "the book title").FirstOrDefault(); if (book == null) { Console.WriteLine("not find the book."); return; } context.Books.Remove(book); int records = await context.SaveChangesAsync(); Console.WriteLine($"{records} records deleted"); } }注意:
对象-关系映射工具,如EF Core,并不适用于所有场景。使用示例代码删除所有对象不那么高效。使用单个SQL语句可以删除所有记录,而不是为每一条记录使用一条DELETE语句。EF Core在这种场景中并没有那么糟糕,因为多个语句可以合并为一个批处理语句,如本章后面所述。
了解了如何添加,查询,更新和删除记录,本章后面将介绍后台的功能,讨论使用Entity Framework的高级场景。
10. 日志记录
为了查看发送到数据库的SQL语句,可以打开SQL Server的分析器,在Visual Studio中打开Intellitrace Events (Debug | Windows |Intellitrace Events),这需要Visual Studio 的企业版,或者只是启用日志记录。使用日志记录,可以在自己喜欢的地方编写跟踪信息。
EF Core在内部使用一个依赖注入容器(使用Microsoft.Extensions.DependencyInjection),它注册了接口ILoggerFactory。可以访问这个接口,并注册自己的日志记录器提供程序。
下面的代码片段使用BooksContext注册一个新的日志记录器。首先,使用GetInfrastructure扩展方法检索上下文的IServiceProvider。这个扩展方法是在名称空间Microsoft.EntityFrameworkCore.Infrastructure中定义的。使用IServiceProvicer,可以检索在容器中注册的服务,例如接口ILoggerFactory。此接口用于在EF Core基础结构中编写日志信息。使用这个接口,可以添加日志提供程序,比如控制台日志提供程序。这个日志提供程序在NuGet包Microsoft.Extensions.Logging.Console中定义。该提供程序为ILoggerFactory定义了AddConsole扩展方法,以便于将其添加为日志提供程序。在这里,日志提供程序配置为编写信息日志:
private static void AddLogging() { using (var context = new BooksContext()) { IServiceProvider provider = context.GetInfrastructure<IServiceProvider>(); ILoggerFactory loggerFactory = provider.GetService<ILoggerFactory>(); loggerFactory.AddConsole(LogLevel.Information); } }需要在只有一个上下文的情况下进行此配置。注册是在EF Core的基础结构中完成的,因此一旦为应用程序配置了这个基础结构,日志记录就会在每个实例化的上下文中完成。例如,前面实现的QueryBooksAsync方法现在在控制台上显示了日志信息:
static async Task Main(string[] args) { AddLogging(); //await CreateTheDatabaseAsync(); //await DeleteDatabaseAsync(); //await AddBookAsync("Hello","World"); //await AddBooksAsync(); //await ReadBooksAsync(); await QueryBooksAsync(); //await UpdateBookAsync(); //await DeleteBooksAsync(); //await DeleteSingleBookAsync(); } info: Microsoft.EntityFrameworkCore.Infrastructure[10403] Entity Framework Core 2.1.1-rtm-30846 initialized 'BooksContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (34ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT [b].[BookId], [b].[Publisher], [b].[Title] FROM [Books] AS [b] WHERE [b].[Publisher] = N'Wrox Press' 1003 Professional C# 6 and .NET Core 1.0 Wrox Press 1004 Professional C# 5 and .NET 4.5.1 Wrox Press 1005 JavaScript for Kids Wrox Press注意:
需要包Microsoft.EntityFrameworkCore的版本为2.1.1,使用最新的发布版本(3.1.5)或预览版(5.0.0-preview.6.20312.4)会发生异常:
System.TypeLoadException HResult=0x80131522 Message=Method 'get_Info' in type 'Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal.SqlServerOptionsExtension' from assembly 'Microsoft.EntityFrameworkCore.SqlServer, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60' does not have an implementation. Source=Microsoft.EntityFrameworkCore.SqlServer StackTrace: at Microsoft.EntityFrameworkCore.SqlServerDbContextOptionsExtensions.UseSqlServer(DbContextOptionsBuilder optionsBuilder, String connectionString, Action`1 sqlServerOptionsAction) at BooksSample.BooksContext.OnConfiguring(DbContextOptionsBuilder optionsBuilder) in D:\C#TrainingSamples\ADODotNet\ConsoleApp1\BooksContext.cs:line 17 at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider() at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.get_Instance() at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetInfrastructure[T](IInfrastructure`1 accessor) at BooksSample.Program.AddLogging() in D:\C#TrainingSamples\ADODotNet\ConsoleApp1\Program.cs:line 175 at BooksSample.Program.<Main>d__0.MoveNext() in D:\C#TrainingSamples\ADODotNet\ConsoleApp1\Program.cs:line 21同时将Microsoft.EntityFrameworkCore.SqlServer也使用预览版(5.0.0-preview.6.20312.4),虽然没有引发上述异常,不过貌似没有正常启动日志提供程序,输出中没有日志信息。
需要Microsoft.ExtensionslLogging.Console的版本为2.1.1,最新的版本(3.1.5)提示该程序集中不包含扩展方法AddConsole();
需要包的版本如下(使用其他版本可能会无法正常得出日志结果,因为后续的包版本移除了为ILoggerFactory定义的AddConsole()扩展方法):
Microsoft.EntityFrameworkCore (2.1.1)Microsoft.EntityFrameworkCore.SqlServer (2.1.1)Microsoft.ExtensionsLogging.Console (2.1.1)