Entity Framework Core

Entity Framework Core

EF历史—Need

EF Core

这里写图片描述
创建WroxBooks——Books表

CREATE TABLE [dbo].[Books]
(
[BookId] INT IDENTITY(1, 1) NOT NULL,
[Title] NVARCHAR(50) NOT NULL,
[Publisher] NVARCHAR(25) NULL,
CONSTRAINT [PK_Books] PRIMARY KEY CLUSTERED ([BookId] ASC)
)

使用依赖注入

BooksContext

public class BooksContext : DbContext
{
    public BooksContext(DbContextOptions<BooksContext> options) : base(options) { }
}

BooksService

public class BooksService
{
    private BooksContext _booksContext;
    public BooksService(BooksContext context) => _booksContext = context;
}

主调程序

class Program
{

    static async Task Main()
    {
        var p = new Program();
        p.InitializeServices();
        p.ConfigureLogging();
        var service = p.Container.GetService<BooksService>();
    }

    /// <summary>
    /// 使用依赖注入
    /// </summary>
    private void InitializeServices()
    {
        const string ConnectionString = @"server=(localdb)\MSSQLLocalDb;database=EFCoreDemoFluentAPI;trusted_connection=true";
        var services = new ServiceCollection();
        services.AddTransient<BooksService>()
            .AddEntityFrameworkSqlServer()
            .AddDbContext<BooksContext>
            (options => options.UseSqlServer(ConnectionString));

        Container = services.BuildServiceProvider();
    }
    public ServiceProvider Container { get; private set; }
    /// <summary>
    /// 添加日志,输出到Console
    /// </summary>
    private void ConfigureLogging()
    {
        ILoggerFactory loggerFactory = Container.GetService<ILoggerFactory>();
        loggerFactory.AddConsole(LogLevel.Information);
    }
}

Model

Convention(约定),Annotation(注释)和Fluent API

EF Core使用三个概念来定义模型:Convention,Annotation和Fluent API。

  • Convention——会自动对数据库进行操作。例如,使用Id后缀命名int类型或Guid类型的属性会将属性映射到主键。
  • Annotation——可以使用注释覆盖约定。
  • Fluent API——可以使用Fluent API代替注释,比注释强大。通过代码完成配置。使用Fluent API,方法的返回值可用于调用下一个方法。

Convention

Book

public class Book
{
    public int BookId { get; set; }
    public string Title { get; set; }
    public List<Chapter> Chapters { get; } = new List<Chapter>();
    /// <summary>
    /// Page会通过Context自动创建实例
    /// </summary>
    public List<Page> Pages { get; set; }
    /// <summary>
    /// 一个作者会有多本书
    /// 在多对一的关系中,Book作为多的一方,User作为一的一方。Book会有User的外键。
    /// 有可能会添加阴影属性AuthorId,外键到指定User中的主键。
    /// </summary>
    public User Author { get; set; }
}

Chapter

public class Chapter
{
    public int ChapterId { get; set; }
    public int Number { get; set; }
    public string Title { get; set; }
    /// <summary>
    /// BookId属性为Book的外键
    /// 如果没有此属性,由于已经有了Book属性,则会按约定创建阴影属性
    /// </summary>
    public int BookId { get; set; }
    public Book Book { get; set; }
}

Page

public class Page
{
    public int PageId { get; set; }
    public string Content { get; set; }
    /// <summary>
    /// BookId属性为Book的外键
    /// 如果没有此属性,由于已经有了Book属性,则会按约定创建阴影属性
    /// </summary>
    public int BookId { get; set; }
    public Book Book { get; set; }
}

User

扫描二维码关注公众号,回复: 3267524 查看本文章
public class User
{
    public int UserId { get; set; }
    public string Name { get; set; }
    public List<Book> AuthoredBooks { get; set; }
}
使用约定的Table per Hierarchy(TPH)

EF Core支持Table per Hierarchy(TPH)关系类型。 通过此关系,可以使用形成层次结构的多个模型类映射到单个表。 可以使用约定和使用fluent API指定此关系。

这里写图片描述

运行应用程序以创建数据库时,只创建了一个表-Payments。 此表定义了一个Discriminator列,它将表中的记录映射到相应的模型类型。
这里写图片描述

SELECT [p].[PaymentId], [p].[Amount], [p].[Discriminator],
[p].[Name],
[p].[CreditcardNumber]
FROM [Payments] AS [p]
WHERE [p].[Discriminator] = N'CreditcardPayment'

当然,在这种情况下,也可以调用上下文属性CreditcardPayments,从而产生相同的查询。

Annotation

Book

/// <summary>
/// Book与Author(作者)、Reviewer(审阅者)、ProjectEditor(编辑)关联。
/// 外键属性类型int?,这使他们为可选的。
/// 如果外键属性为int,通过强制关系,EF Core创建了级联删除; 删除本书时,相关作者,编辑和审阅者也将被删除
/// </summary>
/// 
[Table("Books", Schema = "myAnnotation")]
public class Book
{
    public int BookId { get; set; }
    [Column(TypeName = "Money")]
    public decimal Price { get; set; }
    public string Title { get; set; }
    public List<Chapter> Chapters { get; } = new List<Chapter>();
    /// <summary>
    /// Page会通过Context自动创建实例
    /// </summary>
    public List<Page> Pages { get; set; }
    /// <summary>
    /// 一个作者会有多本书
    /// 在多对一的关系中,Book作为多的一方,User作为一的一方。Book会有User的外键。
    /// 有可能会添加阴影属性AuthorId,外键到指定User中的主键。
    /// </summary>
    public User Author { get; set; }
    public int? ReviewerId { get; set; }
    [ForeignKey(nameof(ReviewerId))]
    public User Reviewer { get; set; }
    public int ProjectEditorId { get; set; }
    [ForeignKey(nameof(ProjectEditorId))]
    public User ProjectEditor { get; set; }
}

Chapter

[Table("Chapters", Schema = "myAnnotation")]
public class Chapter
{
    public int ChapterId { get; set; }
    public int Number { get; set; }
    [MaxLength(120)]
    public string Title { get; set; }
    /// <summary>
    /// BookId属性为Book的外键
    /// 如果没有此属性,由于已经有了Book属性,则会按约定创建阴影属性
    /// </summary>
    public int BookId { get; set; }
    public Book Book { get; set; }
}

Page

[Table("Pages", Schema = "myAnnotation")]
public class Page
{
    public int PageId { get; set; }
    public string Content { get; set; }
    /// <summary>
    /// BookId属性为Book的外键
    /// 如果没有此属性,由于已经有了Book属性,则会按约定创建阴影属性
    /// </summary>
    public int BookId { get; set; }
    public Book Book { get; set; }
}

User

/// <summary>
/// User类与Book类型有多个关联。 
/// WrittenBooks:作者写的书
/// ReviewBooks:审阅者的书。
/// EditedBooks:编辑者的书。
/// 如果相同类型之间存在多于一个的关系。则需要使用InverseProperty对属性进行注释。 
/// </summary>
/// 
[Table("Users",Schema = "myAnnotation")]
public class User
{
    public int UserId { get; set; }
    public string Name { get; set; }
    [InverseProperty("Author")]
    public List<Book> WrittenBooks { get; set; }
    [InverseProperty("Reviewer")]
    public List<Book> ReviewedBooks { get; set; }
    [InverseProperty("ProjectEditor")]
    public List<Book> EditedBooks { get; set; }
}

Fluent API

指定关系的最有效方法是使用Fluent API。

  • 一对一:HasOne和WithOne
  • 一对多:HasOne和WithMany
  • 多对一:HasMany and WithOne

Fluent API的优点:

  • 可以在不添加任何属性的情况下保持实体类型简单,
  • 相对于属性而言Fluent API提供了更多的选项
配置自包含类型(Self-Contained Type Configuration)

DbContext.OnModelCreating方法过长可以使用类型配置类。需要接口IEntityTypeConfiguration,并使用Configure方法。
BookConfiguration

public class BookConfiguration : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        //主键
        builder.ToTable("Books").HasKey(b => b.BookId);
        //自增
        builder.Property(b => b.BookId).ValueGeneratedOnAdd();
        builder.Property(b => b.Title).HasMaxLength(50);

        //关系
        builder.HasMany(b => b.Chapters).WithOne(c => c.Book);
        builder.HasMany(b => b.Pages).WithOne(p => p.Book);

        #region 多个User类型的写法
        builder.HasOne(b => b.Author).WithMany(u => u.AuthoredBooks);
        builder.HasOne(b => b.Editor).WithMany(u => u.EditedBooks);
        #endregion
    }
}
public class BooksContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyConfiguration(new BookConfiguration());
    }
}
Scaffolding a Model from the Database——Need

添加依赖

Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Tools.Dotnet

dotnet ef dbcontext scaffold <CONNECTION> <PROVIDER>

<CONNECTION>  数据库连接串
<PROVIDER>    provider(例如:Microsoft.EntityFrameworkCore.SqlServer)
> dotnet ef dbcontext scaffold "server=(localdb)\MSSQLLocalDb;database=MenuCards;trusted_connection=true" "Microsoft.EntityFrameworkCore.SqlServer"

运行此命令后,可以看到DbContext派生类以及生成的模型类型。 默认情况下,模型的配置使用fluent API完成。 但是,您可以将其更改为使用提供–data-annotations选项的数据注释。 还可以影响生成的上下文类名称以及输出目录。

字段映射(Mapping to Fields)

EF Core不仅可以将表中的列映射到属性,还允许映射到私有字段。 这使得可以创建只读属性并使用在类外无法访问的私有字段。
Page

public class Page
{
    private Page() { }

    public Page(string remark)
    {
        _remark = remark;
    }
    /// <summary>
    /// 可以将表的列映射到私有字段。 这使得可以创建只读属性并使用在类外无法访问的私有字段。
    /// </summary>
    private int _pageId = 0;
    public string Content { get; set; }
    private string _remark;
    public string Remark => _remark;
    /// <summary>
    /// BookId属性为Book的外键
    /// 如果没有此属性,由于已经有了Book属性,则会按约定创建阴影属性
    /// </summary>
    public int BookId { get; set; }
    public Book Book { get; set; }
}

PageConfiguration

internal class PageColumnNames
{
    public const string PageId = nameof(PageId);
}
public class PageConfiguration : IEntityTypeConfiguration<Page>
{
    public void Configure(EntityTypeBuilder<Page> builder)
    {
        builder.ToTable("Pages");
        // _pageId没有相应的属性,使用Property的重载方法,会将数据库中的PageId映射到字段_pageId
        builder.Property<int>(PageId).HasField("_pageId").IsRequired();
        builder.HasKey(PageId);
        builder.Property<int>(PageId).ValueGeneratedOnAdd();
        // 使用HasField方法将Remark属性映射到相应字段。
        builder.Property(b => b.Remark).HasField("_remark").IsRequired(false).HasMaxLength(100);
        builder.HasOne(p=>p.Book).WithMany(b => b.Pages);
    }
}

数据库
这里写图片描述

阴影属性(shadow Property)

将数据库列映射到Model中根本没有的字段。可以在上下文使用实体检索不能在Model中使用的阴影属性。

用途

  • 频繁操作
  • 状态值

**PageConfiguration **

internal class PageColumnNames
{
    public const string LastUpdated = nameof(LastUpdated);
    public const string IsDeleted = nameof(IsDeleted);
    public const string PageId = nameof(PageId);

}
public class PageConfiguration : IEntityTypeConfiguration<Page>
{
    public void Configure(EntityTypeBuilder<Page> builder)
    {
        builder.ToTable("Pages");

        // 阴影属性
        // LastUpdated:实体最后更新的时间。 
        // IsDeleted:逻辑删除还是物理删除。逻辑删除可以撤消以恢复实体并提供历史信息。
        builder.Property<bool>(IsDeleted);
        builder.Property<DateTime>(LastUpdated);
    }
}

SaveChanges

/// <summary>
/// 重写方法 SaveChangesAsync 以自动更新阴影属性LastUpdated、并管理IsDeleted属性(同步方法SaveChanges也要重写)
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
        // 如果状态显示添加、修改、删除,则会使用当前时间更新阴影属性LastUpdated
        foreach (var item in ChangeTracker.Entries<Page>()
        .Where(e =>
        e.State == EntityState.Added ||
        e.State == EntityState.Modified ||
        e.State == EntityState.Deleted))
        {
            //使用EntityEntry的CurrentValues索引器访问模型中没有的阴影属性
            item.CurrentValues[LastUpdated] = DateTime.Now;
            // 将实体的状态由删除状态改为修改状态,并将IsDeleted属性设为true
            if (item.State == EntityState.Deleted)
            {
                item.State = EntityState.Modified;
                item.CurrentValues[IsDeleted] = true;
            }
        }
    return base.SaveChangesAsync(cancellationToken);
}
从属实体(owned entity)

包含从属实体(owned entity)的实体类可以将表拆分为多个实体类型。从属实体(owned entity)不需要主键;

包含从属实体的实体类能映射到

  • 一个表:使用表拆分特性
  • 多个表:它们共享相同的主键。

Page

public class Page
{
    private Page() { }

    public Page(string remark)
    {
        _remark = remark;
    }
    /// <summary>
    /// 可以将表的列映射到私有字段。 这使得可以创建只读属性并使用在类外无法访问的私有字段。
    /// </summary>
    private int _pageId = 0;
    public string Content { get; set; }
    private string _remark;
    public string Remark => _remark;
    /// <summary>
    /// BookId属性为Book的外键
    /// 如果没有此属性,由于已经有了Book属性,则会按约定创建阴影属性
    /// </summary>
    public int BookId { get; set; }
    public Book Book { get; set; }

    public TextFont TextFont { get; set; }
    public TextFont TitleFont { get; set; }
}

PageConfiguration

internal class PageColumnNames
{
    public const string LastUpdated = nameof(LastUpdated);
    public const string IsDeleted = nameof(IsDeleted);
    public const string PageId = nameof(PageId);

}
public class PageConfiguration : IEntityTypeConfiguration<Page>
{
    public void Configure(EntityTypeBuilder<Page> builder)
    {
        builder.ToTable("Pages");

        builder.OwnsOne(p => p.TitleFont).OwnsOne<FontColor>(t => t.FontColor, tbuilder =>
        {
            tbuilder.Property(p => p.FontColorName).HasColumnName("TitleFontColorName");
        });
        builder.OwnsOne(p => p.TextFont).ToTable("TextFont").OwnsOne(a => a.FontColor);
    }
}

测试代码(阴影属性和从属实体)

/// <summary>
/// 验证阴影属性
/// </summary>
/// <returns></returns>
public async Task AddShadowPageBooksAsync(IEnumerable<Book> books)
{
    await _booksContext.Books.AddRangeAsync(books);
    await _booksContext.SaveChangesAsync();
    Console.WriteLine($"ShadowPageBooks添加完毕");
    Console.WriteLine();
}

/// <summary>
/// 删除、有阴影属性isDeleted会设为true
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task DeletePageAsync(int id)
{
    Page p = await _booksContext.Pages.FindAsync(id);
    if (p == null) return;
    _booksContext.Pages.Remove(p);
    await _booksContext.SaveChangesAsync();
    Console.WriteLine("运行DeletePageAsync完毕");
    Console.WriteLine();
}

/// <summary>
/// 验证DeleteBookAsync方法
/// </summary>
/// <returns></returns>
public async Task QueryDeletedPagesAsync()
{
    IEnumerable<Page> deletedPages =
    await _booksContext.Pages
    .Where(b => EF.Property<bool>(b, IsDeleted))
    .ToListAsync();
    foreach (var page in deletedPages)
    {
        Console.WriteLine($"deleted: {page}");
    }
}

顺序执行测试代码
创建的两个表:Pages和TextFont
这里写图片描述
这里写图片描述

表拆分(Table Splitting)——EF Core2.0

表拆分可以将单个数据库表拆分为多个实体类型。同一个表的每个类都需要一个一对一的关系并定义自己的主键。 但是,因为它们共享同一个表,所以也共享相同的主键。

User

public class User
{
    public int UserId { get; set; }
    public string Name { get; set; }
    public List<Book> AuthoredBooks { get; set; }
    public List<Book> EditedBooks { get; set; }
    /// <summary>
    /// 将两个实体User和Address在数据合并为一个表
    /// </summary>
    public Address Address { get; set; }
}

Address

public class Address
{
    public int AddressId { get; set; }
    public string AddressDetail { get; set; }
    public int UserId { get; set; }
    public User User { get; set; }
}

AddressConfiguration

public class AddressConfiguration : IEntityTypeConfiguration<Address>
{
    public void Configure(EntityTypeBuilder<Address> builder)
    {
        
        #region 一个数据库表分为两个实体User和Address
        builder.ToTable("Users");
        #endregion

    }
}

**UserConfiguration **

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.ToTable("Users").HasKey(u => u.UserId);
        builder.Property(u => u.UserId).ValueGeneratedOnAdd();
        builder.Property(u=>u.Name).IsRequired();
        #region 多个List<Book>的写法
            builder.HasMany(u => u.AuthoredBooks).WithOne(b => b.Author);
            builder.HasMany(u => u.EditedBooks).WithOne(b => b.Editor);
            #endregion
            
        #region 表拆分,一个数据库表分为两个实体User和Address
        // 在上下文中,User和Address是两个DbSet属性。
        // 在OnModelCreating方法中,User类与Address使用HasOne和WithOne配置为一对一关系。
        // User和Address传递给ToTable方法的参数指定了相同的表名。 
        // User和Address都映射到同一个表Users。
        //builder.ToTable("Users").HasKey(u => u.UserId); //需要本段代码,可与不设置主键,因为第一行有,所以不重复
        builder.HasOne(u => u.Address).WithOne(a => a.User).HasForeignKey<Address>(a => a.AddressId);
        #endregion
    }
}

这里写图片描述

实体拆分与表拆分相反,其中实体被拆分为多个表。 EF Core 2.0尚未提供此功能,但计划使用更高版本的EF Core。

使用Fluent API的Table per Hierarchy

使用Fluent API,可以更好地控制层次结构。

The database created looks similar like before, just instead of the
Discriminator column the table Payments now defines the Type column
—as specified with the creation of the model. The new query asking for
credit card numbers filters the Type column:

SELECT [p].[PaymentId], [p].[Amount], [p].[Name], [p].[Type],
[p].[CreditcardNumber]
FROM [Payments] AS [p]
WHERE [p].[Type] = N'Creditcard'

Context

Convention(约定),Annotation(注释)

public class BooksContext :DbContext
{
    private const string ConnectionString = @"server=(localdb)\MSSQLLocalDb;database=EFCoreDemo;trusted_connection=true";

    /// <summary>
    /// get:允许查询
    /// set:允许添加
    /// </summary>
    public DbSet<Book> Books { get; set; }
    public DbSet<Chapter> Chapters { get; set; }
    public DbSet<User> Users { get; set; }
    public DbSet<User> Pages { get; set; }
    /// <summary>
    /// 重写DbContext的OnConfiguring方法可以定义连接字符串。
    /// </summary>
    /// <param name="optionsBuilder"></param>
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        
        //UseSqlServer扩展方法将上下文映射到SQL Server数据库。
        optionsBuilder.UseSqlServer(ConnectionString);
    }
}

还可以使用依赖注入定义数据库连接串

Fluent API

public class BooksContext : DbContext
{
    /// <summary>
    /// 使用依赖注入
    /// </summary>
    /// <param name="options"></param>
    public BooksContext(DbContextOptions<BooksContext> options) : base(options) { }

    public DbSet<Page> Pages { get; set; }
    public DbSet<Book> Books { get; set; }
    public DbSet<Chapter> Chapters { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.HasDefaultSchema("fluent");
        modelBuilder.ApplyConfiguration(new BookConfiguration());
        modelBuilder.ApplyConfiguration(new PageConfiguration());
        modelBuilder.ApplyConfiguration(new ChapterConfiguration());
        modelBuilder.ApplyConfiguration(new UserConfiguration());
        modelBuilder.ApplyConfiguration(new AddressConfiguration());
    }

    /// <summary>
    /// 重写方法 SaveChangesAsync 以自动更新阴影属性LastUpdated、并管理IsDeleted属性(同步方法SaveChanges也要重写)
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        #region 阴影属性,为Page的阴影属性做处理
        // 如果状态显示添加、修改、删除,则会使用当前时间更新阴影属性LastUpdated
        foreach (var item in ChangeTracker.Entries<Page>()
            .Where(e =>
            e.State == EntityState.Added ||
            e.State == EntityState.Modified ||
            e.State == EntityState.Deleted))
        {
            //使用EntityEntry的CurrentValues索引器访问模型中没有的阴影属性
            item.CurrentValues[LastUpdated] = DateTime.Now;
            // 将实体的状态由删除状态改为修改状态,并将IsDeleted属性设为true
            if (item.State == EntityState.Deleted)
            {
                item.State = EntityState.Modified;
                item.CurrentValues[IsDeleted] = true;
            }
        }
        #endregion

        return base.SaveChangesAsync(cancellationToken);
    }
}

依赖注入

主程

public ServiceProvider Container { get; private set; }
/// <summary>
/// 使用依赖注入
/// </summary>
private void InitializeServices()
{
    const string ConnectionString = @"server=(localdb)\MSSQLLocalDb;database=EFCoreDemoFluentAPI;trusted_connection=true";
    var services = new ServiceCollection();
    services.AddTransient<BooksService>()
        .AddEntityFrameworkSqlServer()
        .AddDbContext<BooksContext>
        (options => {
            options.EnableSensitiveDataLogging();
            options.UseSqlServer(ConnectionString); }
        );

    Container = services.BuildServiceProvider();
}

添加日志

主程

/// <summary>
/// 添加日志,输出到Console
/// </summary>
private void ConfigureLogging()
{
    ILoggerFactory loggerFactory = Container.GetService<ILoggerFactory>();
    loggerFactory.AddConsole(LogLevel.Information);
}

Context pooling(上下文池)——EF Core 2.0

EF Core 2.0中,使用上下文池(context pooling)可以提高性能。数据库连接应在需要时打开,使用后关闭。此行为已在在EF Core中实现。正在关闭连接时(On closing),数据库连接并未真正关闭。实际上,连接回到池中,等待重用。

DB context应该在需要之前创建,并在使用后立即关闭(销毁)。这种开销并不像您预期​​的那么重要。每次重新调用 context 时,都不会重新初始化模型;相反,模型被重用。使用Entity Framework和XML文件映射,创建context 的开销比使用EF Core时要大得多。但是,当有大的 context 时,创建context的开销可能仍然很大。在这里,您可以使用Context pooling来提高性能。

要使用上下文池,必须使用依赖注入。要激活Context pooling,您需要做的就是将EF Core注册从AddDbContext更改为AddDbContextPool。这样,从Context pooling中检索注入的Context

var services = new ServiceCollection();
services.AddTransient<BooksController>();
services.AddTransient<BooksService>();
services.AddEntityFrameworkSqlServer();
services.AddDbContextPool<BooksContext>(options =>
options.UseSqlServer(ConnectionString));
services.AddLogging();
Container = services.BuildServiceProvider();

事务

每次使用实体框架访问数据库时,都会涉及一个事务。 您可以隐式使用事务,也可以根据需要使用配置显式创建事务。

隐式事务

SaveChanges方法的调用会自动解析为一个事务。 如果更改的一部分失败——例如,由于数据库约束——所有已完成的更改都将回滚。

/// <summary>
/// 隐式事务,添加失败
/// </summary>
public void AddTwoRecordsWithOneTx()
{
    try
    {
        Book book = _booksContext.Books.First();

        var chapter = new Chapter()
        {
            BookId = book.BookId,
            Title = "TxChapterAdded",
            Number = 1
        };
        // 取得现有book表中最后一条id
        int hightestId = _booksContext.Books.Max(c => c.BookId);
        var pInvalid = new Chapter
        {
            //BookId赋予了一个无效的id
            BookId = ++hightestId,
            Title = "invalid"
        };
        _booksContext.Chapters.AddRange(chapter, pInvalid);
        int records = _booksContext.SaveChanges();
        Console.WriteLine($"添加了{records}条");
    }
    catch (DbUpdateException ex)
    {
        Console.WriteLine($"{ex.Message}");
        Console.WriteLine($"{ex?.InnerException.Message}");
    }
    Console.WriteLine();
}

异常的详细信息

An error occurred while updating the entries. See the inner exception for details.
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Chapters_Books_BookId". The conflict occurred in database "EFCoreDemoFluentAPI", table "fluent.Books", column 'BookId'.
The statement has been terminated.

多次调用SaveChanges,实现第一条插入成功,第二天插入失败。

/// <summary>
/// 多次调用savechanges
/// </summary>
public void AddTwoRecordsWithTwoTx()
{
    try
    {
        Book book = _booksContext.Books.First();

        var chapter = new Chapter()
        {
            BookId = book.BookId,
            Title = "TxChapterAdded",
            Number = 1
        };
        _booksContext.Chapters.Add(chapter);
        int records = _booksContext.SaveChanges();
        Console.WriteLine($"添加了{records}条");
        // 取得现有book表中最后一条id
        int hightestId = _booksContext.Books.Max(c => c.BookId);
        var pInvalid = new Chapter
        {
            //BookId赋予了一个无效的id
            BookId = ++hightestId,
            Title = "invalid"
        };
        _booksContext.Chapters.Add(pInvalid);
        records = _booksContext.SaveChanges();
        Console.WriteLine($"添加了{records}条");
    }
    catch (DbUpdateException ex)
    {
        Console.WriteLine($"{ex.Message}");
        Console.WriteLine($"{ex?.InnerException.Message}");
    }
    Console.WriteLine();
}

第一条插入成功,第二天插入失败

添加了1条
An error occurred while updating the entries. See the inner exception for details.
failThe INSERT statement conflicted with the FOREIGN KEY constraint "FK_Chapters_Books_BookId". The conflict occurred in database "EFCoreDemoFluentAPI", table "fluent.Books", column 'BookId'.
The statement has been terminated.

显式事务

使用显示事务,可以选择在某些业务逻辑失败时回滚,并且可以在一个事务中绑定多次调用SaveChanges。
要启动DbContext派生类的事务,需要调用DbContext.Database.BeginTransaction(),该方法返回IDbContextTransaction。

/// <summary>
/// 显示事务
/// </summary>
/// <returns></returns>
public void  TwoSaveChangesWithOneTx()
{
    IDbContextTransaction tx = null;
    try
    {
        using (tx =  _booksContext.Database.BeginTransaction())
        {
            Book book = _booksContext.Books.First();
            var chapter = new Chapter()
            {
                BookId = book.BookId,
                Title = "TxChapterAdded",
                Number = 1
            };
            _booksContext.Chapters.Add(chapter);
            int records = _booksContext.SaveChanges();
            Console.WriteLine($"添加了{records}条");
            // 取得现有book表中最后一条id
            int hightestId = _booksContext.Books.Max(c => c.BookId);
            var pInvalid = new Chapter
            {
                //BookId赋予了一个无效的id
                BookId = ++hightestId,
                Title = "invalid"
            };
            _booksContext.Chapters.Add(pInvalid);
            records = _booksContext.SaveChanges();
            Console.WriteLine($"添加了{records}条");
            tx.Commit();
        }
    }
    catch (DbUpdateException ex)
    {
        Console.WriteLine($"{ex.Message}");
        Console.WriteLine($"{ex?.InnerException.Message}");
        Console.WriteLine("rolling back…");
        tx.Rollback();
    }
    Console.WriteLine();
}

第一次SaveChanges方法显示了一条添加记录,但此记录将稍后基于Rollback删除。 根据隔离级别(isolation level)的设置,更新的记录只能在事务中完成回滚之前查看,但不能在事务之外查看。

添加了1条
An error occurred while updating the entries. See the inner exception for details.
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Chapters_Books_BookId". The conflict occurred in database "EFCoreDemoFluentAPI", table "fluent.Books", column 'BookId'.
The statement has been terminated.
rolling back…

使用BeginTransaction方法,您设置隔离级别(isolation level)用来指定数据库中所需的隔离要求和锁定。

冲突处理

如果多个用户在同一记录上工作,则需要考虑解决冲突。有很多不同的方法来处理这个问题。 最简单的是最后一个获胜。 最后保存数据的用户会覆盖先前执行更改的用户的更改。

EF Core还提供了让第一个成功的方法。 使用此选项,在保存记录时,如果最初读取的数据仍在数据库中,则需要进行验证。 如果是这种情况,保存数据可以继续,因为读取和写入之间没有发生变化。 但是,如果数据发生变化,则需要进行冲突解决。 让我们进入这些不同的选择。

保留最后一个

默认最后保存最后一条更改。

private const string BookTitle = "sample book";
private const string ConnectionString = @"server=(localdb)\MSSQLLocalDb;database=EFCoreDemoFluentAPI;trusted_connection=true";

/// <summary>
/// 最后一条更改为最终更改(数据库数据更改为最后一条语句)—默认
/// </summary>
public void ConflictHandling()
{
    var options = new DbContextOptionsBuilder<BooksContext>();
    options.EnableSensitiveDataLogging();
    options.UseSqlServer(ConnectionString);
    // 准备初始数据
    void PrepareBook()
    {
        using (var context = new BooksContext(options.Options))
        {
            context.Books.Add(new Book() { Title = BookTitle });
            context.SaveChanges();
        }
    }

    PrepareBook();

    // user 1
    var tuple1 = PrepareUpdate();
    tuple1.book.Title = "用户1更新了这条";

    // user 2
    var tuple2 = PrepareUpdate();
    tuple2.book.Title = "用户2更新了这条";

    Update(tuple1.context, tuple1.book, "用户1");
    Update(tuple2.context, tuple2.book, "用户2");

    tuple1.context.Dispose();
    tuple2.context.Dispose();

    CheckUpdate(tuple1.book.BookId);
}

/// <summary>
/// 返回一个元组,包括context和book。
/// 此方法被调用两次,并返回与不同上下文对象关联的不同Book对象
/// </summary>
/// <returns></returns>
private (BooksContext context, Book book) PrepareUpdate()
{
    var options = new DbContextOptionsBuilder<BooksContext>();
    options.EnableSensitiveDataLogging();
    options.UseSqlServer(ConnectionString);
    var context = new BooksContext(options.Options);
    Book book = context.Books.Where(b => b.Title == BookTitle).FirstOrDefault();
    return (context, book);
}

/// <summary>
/// 将具有指定ID的书籍写入控制台
/// </summary>
/// <param name="id"></param>
private void CheckUpdate(int id)
{
    var options = new DbContextOptionsBuilder<BooksContext>();
    options.EnableSensitiveDataLogging();
    options.UseSqlServer(ConnectionString);

    using (var context = new BooksContext(options.Options))
    {
        Book book = context.Books.Find(id);
        Console.WriteLine($"this is the updated state: {book.Title}");
    }
}

第一次更新和第二次更新都执行更新成功,第二次更新只会覆盖第一次更新中的数据,

用户1: 1 record updated from 用户1
用户2: 1 record updated from 用户2
this is the updated state: 用户2更新了这条

保留第一个

Book

public class Book
{
	//...
	#region 解决冲突_保留第一个更改
    public byte[] TimeStamp { get; set; }
    #endregion
}

BookConfiguration

public void Configure(EntityTypeBuilder<Book> builder)
{
	//...
    #region 解决冲突_保留第一个更改,和保存最后一个更改会有冲突
    builder.Property(p => p.TimeStamp).HasColumnType("timestamp")
        .ValueGeneratedOnAddOrUpdate().IsConcurrencyToken();
    #endregion
}

String扩展方法

public static class ByteArrayExtension
{
    public static string StringOutput(this byte[] data)
    {
        var sb = new StringBuilder();
        foreach (byte b in data)
        {
            sb.Append($"{b}.");
        }
        return sb.ToString();
    }
}

示例方法

private const string BookTitle = "sample book";
private const string ConnectionString = @"server=(localdb)\MSSQLLocalDb;database=EFCoreDemoFluentAPI;trusted_connection=true";

public void ConflictHandingFirst()
{
    var options = new DbContextOptionsBuilder<BooksContext>();
    options.EnableSensitiveDataLogging();
    options.UseSqlServer(ConnectionString);
    // 准备初始数据
    void PrepareBook()
    {
        using (var context = new BooksContext(options.Options))
        {
            context.Books.Add(new Book() { Title = BookTitle });
            context.SaveChanges();
        }
    }
    PrepareBook();


    (BooksContext context, Book book) PrepareUpdateFirst()
    {

        var pOptions = new DbContextOptionsBuilder<BooksContext>();
        pOptions.EnableSensitiveDataLogging();
        pOptions.UseSqlServer(ConnectionString);
        var context = new BooksContext(pOptions.Options);
        Book book = context.Books.Where(b => b.Title == BookTitle).FirstOrDefault();
        return (context, book);
    }

    // user 1
    var tuple1 = PrepareUpdateFirst();
    tuple1.book.Title = "用户1更新了这条";

    // user 2
    var tuple2 = PrepareUpdateFirst();
    tuple2.book.Title = "用户2更新了这条";

    UpdateFirst(tuple1.context, tuple1.book, "用户1");
    UpdateFirst(tuple2.context, tuple2.book, "用户2");

    tuple1.context.Dispose();
    tuple2.context.Dispose();

    CheckUpdateFirst(tuple1.book.BookId);
}


private void UpdateFirst(BooksContext context, Book book,string user) {
    try
    {
        Console.WriteLine($"{user}: 更新中: id {book.BookId}, timestamp {book.TimeStamp.StringOutput()}");
        ShowChangesFirst(book.BookId, context.Entry(book));
        int records = context.SaveChanges();
        Console.WriteLine($"{user}: 已更新 {book.TimeStamp.StringOutput()}");
        Console.WriteLine($"{user}: 已更新 {records} 条 updated while updating {book.Title}");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        Console.WriteLine($"{user}: 使用{book.Title}更新失败");
        Console.WriteLine($"{user}: error: {ex.Message}");
        foreach (var entry in ex.Entries)
        {
            if (entry.Entity is Book b)
            {
                Console.WriteLine($"{b.Title} {b.TimeStamp.StringOutput()}");
                ShowChangesFirst(book.BookId, context.Entry(book));
            }
        }
    }

}

private void ShowChangesFirst(int id, EntityEntry entity) {
    //多次调用
    void ShowChange(PropertyEntry propertyEntry) => 
        Console.WriteLine($"id:{id},CurrentValue:{propertyEntry.CurrentValue}," +
        $"original: {propertyEntry.OriginalValue}, modified: {propertyEntry.IsModified}");

    ShowChange(entity.Property("Title"));
}

private static void CheckUpdateFirst(int id)
{
    var options = new DbContextOptionsBuilder<BooksContext>();
    options.EnableSensitiveDataLogging();
    options.UseSqlServer(ConnectionString);

    using (var context = new BooksContext(options.Options))
    {
        Book book = context.Books.Find(id);
        Console.WriteLine($"已更新状态: {book.Title}");
    }

结果

用户1: 更新中: id 10, timestamp 0.0.0.0.0.0.23.119.
id:10,CurrentValue:用户1更新了这条,original: sample book, modified: True
用户1: 已更新 0.0.0.0.0.0.23.120.
用户1: 已更新 1 条 updated while updating 用户1更新了这条
用户2: 更新中: id 10, timestamp 0.0.0.0.0.0.23.119.
id:10,CurrentValue:用户2更新了这条,original: sample book, modified: True
用户2: 使用用户2更新了这条更新失败
用户2: error: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
用户2更新了这条 0.0.0.0.0.0.23.119.
id:10,CurrentValue:用户2更新了这条,original: sample book, modified: True
已更新状态: 用户1更新了这条

数据库创建与删除

创建

public async Task CreateDatabaseAsync()
{
    bool isCreated = await _booksContext.Database.EnsureCreatedAsync();
    string res = isCreated ? "创建完毕" : "已创建";
    Console.WriteLine($"数据库创建:{res}");
}

删除

public async Task DeleteDatabaseAsync()
{
    bool isDeleted = await _booksContext.Database.EnsureDeletedAsync();
    string res = isDeleted ? "删除完毕" : "无此数据库";
    Console.WriteLine($"数据库删除:{res}");
}

查询

在调试期间打开IntelliTrace事件窗口时,您可以看到发送到数据库的SQL语句(这需要Visual Studio Enterprise版本):

基本查询

/// <summary>
/// 根据Id查询book
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private async Task<Book> QueryBookAsync(int id)
{
    return await _booksContext.FindAsync<Book>(id);
    //return await _booksContext.Books.FirstOrDefaultAsync(b=>b.BookId == id);
}
/// <summary>
/// 查询所有Book
/// </summary>
/// <returns></returns>
private async Task QueryAllBooksAsync()
{
    List<Book> books = await _booksContext.Books.ToListAsync();
    foreach (var b in books)
    {
        Console.WriteLine(b);
    }
    // 使用异步API时,可以使用从ToAsyncEnumerable方法返回的IAsyncEnumerable,并使用ForEachAsync
    //await context.Books.ToAsyncEnumerable().ForEachAsync(b =>
    //{
    //    Console.WriteLine(b);
    //});
}

Where方法可以简单过滤语句。 还可以在Where使用Contains方法。 因为Where使用得失延迟加载,所以没有异步方法。
foreach会触发查询的执行并阻塞线程,直到检索到结果。 而使用Task.ToListAsync可以立即执行。

List<Book> books = await context.Books.Where(b => b.Title.Contains(title)).ToListAsync();

原始Sql查询

private async Task RawSqlQuery(string title)
{
    IList<Book> books = await _booksContext.Books.FromSql(
    $"SELECT * FROM fluent.Books WHERE Title = {title}")
    .ToListAsync();
    foreach (var b in books)
    {
        Console.WriteLine(b.ToString());
    }
}

编译查询——需要重复执行的查询

对于需要重复执行的查询,可以使用EF.CompileQuery创建编译查询进行。

/// <summary>
/// 编译查询
/// </summary>
public void CompiledQuery(string qTitle)
{
    Func<BooksContext, string, IEnumerable<Book>> query =
        EF.CompileQuery<BooksContext, string, Book>((context, title) =>
        context.Books.Where(b => b.Title == title));

    IEnumerable<Book> books = query(_booksContext, qTitle);
    foreach (var b in books)
    {
        Console.WriteLine(b.ToString());
    }
}

可以为创建编译查询作为一个成员字段(create a compiled query to a member field ),以便在以后需要时使用它,并且通过传递不同下上文进行查询。

EF.Functions

EF Core允许provider 实现的自定义扩展方法。 为此,EF类定义了可以使用extension method扩展的DbFunction类型的Functions属性。

/// <summary>
/// EF.Functions
/// 通过使用EF.Functions.Like增强了Where方法的查询,并提供包含参数titleSegment的表达式。 
/// 参数titleSegment嵌入在两个%字符内
/// </summary>
/// <param name="titleSegment"></param>
/// <returns></returns>
public async Task UseEFCunctions(string titleSegment)
{
    string likeExpression = $"%{titleSegment}%";
    IList<Book> books = await _booksContext.Books.Where(
    b => EF.Functions.Like(b.Title,
    likeExpression)).ToListAsync();
    foreach (var b in books)
    {
        Console.WriteLine(b.ToString());
    }
}

运行应用程序时,包含EF.Functions.Like的方法将转换为带有LIKE的SQL子句WHERE:

SELECT [b].[BookId], [b].[AuthorUserId], [b].[EditorUserId], [b].[Title], [b].[Type], [b].[AgeLimit]
FROM [fluent].[Books] AS [b]
WHERE [b].[Type] IN (N'MinorBook', N'AdultBook', N'Book') AND [b].[Title] LIKE @__likeExpression_1

显式加载

查询对象并显示相关属性——使用显示加载

/// <summary>
/// 显示加载,每load一次,对应的表就会进行一次查询。
/// </summary>
/// <param name="startsWithTitle"></param>
public void ExplicitLoading(string startsWithTitle)
{
    var book = _booksContext.Books.Where(b => b.Title.StartsWith(startsWithTitle)).FirstOrDefault();
    if (book != null)
    {
        _booksContext.Entry(book).Collection(b => b.Pages).Load();
        _booksContext.Entry(book).Reference(b => b.Author).Load();
        Console.WriteLine(book.Author.Name);
        foreach (var page in book.Pages)
        {
            Console.WriteLine(page.Content);
        }
    }
}

实现Load方法的NavigationEntry类还实现了一个IsLoaded属性,可以在其中检查是否已加载关系。 在调用Load方法之前,您不需要检查加载的关系; 如果在调用Load方法时已经加载了关系,则对数据库的查询不会再次发生。

// 使用查询书籍运行应用程序时,将在SQL Server上执行以下SELECT语句。 此查询仅访问Books表:
SELECT TOP(1) [b].[BookId], [b].[AuthorUserId], [b].[EditorUserId], [b].[Title], [b].[Type], [b].[AgeLimit]
FROM [fluent].[Books] AS [b]
WHERE [b].[Type] IN (N'MinorBook', N'AdultBook', N'Book') AND (([b].[Title] LIKE @__startsWithTitle_0 + N'%' AND (LEFT([b].[Title], LEN(@__startsWithTitle_0)) = @__startsWithTitle_0)) OR (@__startsWithTitle_0 = N''))

// 使用以下Load方法检索书籍的page,SELECT语句根据书籍ID检索章节:
SELECT [p].[PageId], [p].[BookId], [p].[Content], [p].[IsDeleted], [p].[LastUpdated], [p].[Remark], [p].[PageId], [p].[TitleFont_FontName], [p].[PageId], [p].[TitleFontColorName], [e.TextFont].[PageId], [e.TextFont].[FontName], [e.TextFont].[PageId], [e.TextFont].[FontColor_FontColorName]
FROM [fluent].[Pages] AS [p]
LEFT JOIN [fluent].[TextFont] AS [e.TextFont] ON [p].[PageId] = [e.TextFont].[PageId]
WHERE ([p].[IsDeleted] = 0) AND ([p].[BookId] = @__get_Item_0)

// 第三个查询,从Users表中检索用户信息:
SELECT [e].[UserId], [e].[Name]
FROM [fluent].[Users] AS [e]
WHERE [e].[UserId] = @__get_Item_0

立即加载

通过调用Include方法来实现立即加载。

/// <summary>
/// 立即加载(急切加载)
/// </summary>
/// <param name="startsWithTitle"></param>
public void EagerLoading(string startsWithTitle) {
    var book = _booksContext.Books.Include(b => b.Author)
        .Include(b => b.Pages).Where(b => b.Title.StartsWith(startsWithTitle)).FirstOrDefault();
    if (book != null)
    {
        Console.WriteLine(book.Author.Name);
        foreach (var page in book.Pages)
        {
            Console.WriteLine(page.Content);
        }
    }
}

使用Include,只执行一个SQL语句来访问Books表并加入Pages和Users表:

如果需要包含多级关系,则可以在Include方法的结果上使用ThenInclude方法。

全局查询过滤器

Page的IsDeleted列使用了阴影状态。 当IsDeleted为true时,不需要为每个查询定义where。代替的是可以在创建Model时定义全局查询过滤器

builder.HasQueryFilter(p => !EF.Property<bool>(p, IsDeleted));

通过定义此查询过滤器。检查IsDeleted的Where语句将会添加到每个使用这个上下文的查询。

全局查询过滤器也具有多租户技术要求的实际用途(Global query filters are also of practical use with multi-tenancy requirements. )。 可以为特定的tenant-id为上下文筛选所有查询。 您只需要在构造上下文时传递tenant-id。
不使用依赖注入,可以将tenant-id传递给构造函数。
使用依赖项注入,您只需指定一个使用构造函数注入的服务,其中可以在查询过滤器中检索tenant-id。

multi-tenancy technology(多租户技术或称多重租赁技术),是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性。

可以忽略全局查询过滤器。 例如,要获取所有已删除的实体,可以将方法IgnoreQueryFilters应用于LINQ表达式。

客户端和服务端评估

查询并非所有的部分都可以转换为Sql语句并在服务器上运行。某些部分需要在客户端上运行。EF Core允许透明的客户端和服务器评估。例如,能在服务器上完全评估查询的一个提供者。使用不解析所有查询的另一个提供程序,程序仍然运行,但是现在在客户机上评估了一些部分。

举例: Book与Author相关。一本书可以由多位作者撰写,一位作者可以写多本书。执行以下代码。

var books = context.Books
	.Where(b => b.Title.StartsWith("Pro"))
	.OrderBy(b => b.Title)
	.Select(b => new
	{
		b.Title,
		Authors = b.BookAuthors
	});

所有的代码都能使用EF Core 2.0转换为SQL语句。评估只在服务器上运行。

SELECT [b.BookAuthors].[BookId], [b.BookAuthors].[AuthorId]
FROM [BookAuthors] AS [b.BookAuthors]
INNER JOIN (
	SELECT [b0].[BookId], [b0].[Title]
	FROM [Books] AS [b0]
	WHERE [b0].[Title] LIKE N'Pro' + N'%' AND (LEFT([b0].[Title], LEN(N'Pro')) = N'Pro')) AS [t] ON [b.BookAuthors].[BookId] = [t].[BookId]
ORDER BY [t].[Title], [t].[BookId]

如果Select语句修改为返回包含作者的逗号分隔字符串,则结果会不同。

将字符串分配给Authors属性。使用BookAuthors,只选择作者的FirstName和LastName属性,string.Join将列表连接到单个字符串

var books = context.Books
	.Where(b => b.Title.StartsWith("Pro"))
	.OrderBy(b => b.Title)
	.Select(b => new
	{
		b.Title,
		Authors = string.Join(", ", b.BookAuthors.Select(a => $"{a.Author.FirstName} {a.Author.LastName}").ToArray())
});

EF Core 2.0无法将此查询转换为SQL语句。来自EF Core的记录信息显示此警告:

warn: Microsoft.EntityFrameworkCore.Query[200500]
The LINQ expression 'join Author a.Author in value(
Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[
BooksSample.Author]) on Property([a], "AuthorId")
equals
Property([a.Author], "AuthorId")'
could not be translated and will be evaluated locally.

现在执行了三个查询。这些查询的结果将在客户端上加入。该应用程序仍然有效,但查询效率不高。在SQL Server中执行三个语句而不是一个语句,在分析查询时,您可以看到在客户端上进行评估之前从服务器检索所有作者。这可能会导致向客户端的大量传输:

SELECT [b].[Title], [b].[BookId]
FROM [Books] AS [b]
WHERE [b].[Title] LIKE N'Pro' + N'%' AND (LEFT([b].[Title], LEN(N'Pro')) = N'Pro')
ORDER BY [b].[Title]

SELECT [b0].[BookId], [b0].[AuthorId]
FROM [BookAuthors] AS [b0]
WHERE @_outer_BookId = [b0].[BookId]

SELECT [a.Author].[AuthorId], [a.Author].[FirstName],[a.Author].[LastName]
FROM [Authors] AS [a.Author]

自动进行客户端和服务器评估是切实可行的。与EF Core 1.0不同,EF Core 2.0的SQL Server提供程序可以在服务器上进行更多评估,未来版本甚至可以在服务器上支持更多评估。使用其他提供商可能会有不同的结果效率不一样,但至少程序是有效的。

要避免在服务器上进行评估,可以将上下文配置为仅在服务器上进行评估时抛出异常。您可以在配置上下文时通过在optionsBuilder上调用ConfigureWarnings方法来执行此操作:

optionsBuilder.UseSqlServer(ConnectionString)
	.ConfigureWarnings(warnings =>
		warnings.Throw(RelationalEventId.QueryClientEvaluationWarning));

客户端和服务器评估是使您的程序跨不同提供程序工作的一个很好的功能。但是,这可能会导致性能下降。要定义查询以获得最佳性能,可以通过配置抛出异常,轻松找出客户端何时进行评估。然后,可以相应地更改查询。

保存

保存

/// <summary>
/// 关联表的保存
/// </summary>
public void AddRecords()
{
    var book = new Book()
    {
        Title = "SaveBook1",
        Pages = new List<Page>()
                   {
                       new Page("Remark1_1")
                       {
                           Content ="Content1_1",
                           TitleFont = new TextFont(){
                               FontName = "TitleFontName1_1",
                               FontColor = new FontColor(){ FontColorName = "TitleFontColorName1_1" }
                           },
                           TextFont = new TextFont()
                           {
                               FontName = "TextFontName1_1",
                               FontColor = new FontColor(){ FontColorName = "TextFontColorName1_1" }
                           }
                       },
                       new Page("Remark1_2")
                       {
                           Content ="Content1_2",
                           TitleFont = new TextFont(){
                               FontName = "TitleFontName1_2",
                               FontColor = new FontColor(){ FontColorName = "TitleFontColorName1_2" }
                           },
                           TextFont = new TextFont()
                           {
                               FontName = "TextFontName1_2",
                               FontColor = new FontColor(){ FontColorName = "TextFontColorName1_2" }
                           }
                       },
                   },
        Author = new User()
        {
            Name = "SaveAuthor",
            Address = new Address()
            {
                AddressDetail = "SaveAddressDetail"
            }
        }
    };
    
    _booksContext.Books.Add(book);
    ShowState();
    int records = _booksContext.SaveChanges();
    Console.WriteLine($"{records} added");
}

/// <summary>
/// DbContext.ChangeTracker.Entries 返回所有更改追踪器知道的所有对象
/// </summary>
/// <param name="context"></param>
private void ShowState()
{
    //ChangeTracker.Entries,returns all the objects the change tracker knows about. 
    foreach (EntityEntry entry in _booksContext.ChangeTracker.Entries())
    {
        Console.WriteLine($"type: {entry.Entity.GetType().Name}," +
        $"state: {entry.State}, {entry.Entity}");
    }
    Console.WriteLine();
}
type: Book,state: Added, Id:-2147482647,Title:SaveBook1
type: User,state: Added, EFCoreModelUsingFluentAPI.Models.User
type: Address,state: Added, EFCoreModelUsingFluentAPI.Models.Address
type: Page,state: Added, PageId:-2147482647,ContentContent1_1
type: Page,state: Added, PageId:-2147482646,ContentContent1_2
type: TextFont,state: Added, EFCoreModelUsingFluentAPI.Models.TextFont
type: TextFont,state: Added, EFCoreModelUsingFluentAPI.Models.TextFont
type: FontColor,state: Added, EFCoreModelUsingFluentAPI.Models.FontColor
type: FontColor,state: Added, EFCoreModelUsingFluentAPI.Models.FontColor
type: TextFont,state: Added, EFCoreModelUsingFluentAPI.Models.TextFont
type: TextFont,state: Added, EFCoreModelUsingFluentAPI.Models.TextFont
type: FontColor,state: Added, EFCoreModelUsingFluentAPI.Models.FontColor
type: FontColor,state: Added, EFCoreModelUsingFluentAPI.Models.FontColor

对象追踪

上文了解到上下文是如何添加对象的。但是,还需要知道上下文是如何修改对象的。为了了解修改,检索的每个对象都需要在上下文中显示其状态。 为了在action中能看到,让我们创建两个返回同一对象的不同查询。

public void ObjectTracking()
{
    var b1 = (from b in _booksContext.Books
              where b.Title.StartsWith("Save")
              select b).FirstOrDefault();
    var b2 = (from b in _booksContext.Books
              where b.Title.Contains("(")
              select b).FirstOrDefault();
    if (object.ReferenceEquals(b1, b2))
    {
        Console.WriteLine("相同对象");
    }
    else {
        Console.WriteLine("不同对象");

    }
    ShowState();
}
//第一个查询
SELECT TOP(1) [b].[BookId], [b].[AuthorUserId], [b].[EditorUserId], [b].[Title], [b].[Type], [b].[AgeLimit]
FROM [fluent].[Books] AS [b]
WHERE [b].[Type] IN (N'MinorBook', N'AdultBook', N'Book') AND ([b].[Title] LIKE N'Save' + N'%' AND (LEFT([b].[Title], LEN(N'Save')) = N'Save'))
//第二个查询
SELECT TOP(1) [b].[BookId], [b].[AuthorUserId], [b].[EditorUserId], [b].[Title], [b].[Type], [b].[AgeLimit]
FROM [fluent].[Books] AS [b]
WHERE [b].[Type] IN (N'MinorBook', N'AdultBook', N'Book') AND (CHARINDEX(N'(', [b].[Title]) > 0)

相同对象

type: Book,state: Unchanged, Id:1,Title:SaveBook1(Tracker)
type: User,state: Unchanged, EFCoreModelUsingFluentAPI.Models.User
type: Address,state: Unchanged, EFCoreModelUsingFluentAPI.Models.Address
type: Page,state: Unchanged, PageId:1,ContentContent1_1
type: Page,state: Unchanged, PageId:2,ContentContent1_2
type: TextFont,state: Unchanged, EFCoreModelUsingFluentAPI.Models.TextFont

type: TextFont,state: Unchanged, EFCoreModelUsingFluentAPI.Models.TextFont
type: FontColor,state: Unchanged, EFCoreModelUsingFluentAPI.Models.FontColor
type: FontColor,state: Unchanged, EFCoreModelUsingFluentAPI.Models.FontColor
type: TextFont,state: Unchanged, EFCoreModelUsingFluentAPI.Models.TextFont
type: TextFont,state: Unchanged, EFCoreModelUsingFluentAPI.Models.TextFont
type: FontColor,state: Unchanged, EFCoreModelUsingFluentAPI.Models.FontColor
type: FontColor,state: Unchanged, EFCoreModelUsingFluentAPI.Models.FontColor
// 想不跟踪从数据库运行查询的对象,可以使用DbSet调用AsNoTracking方法
var b1 = (from b in _booksContext.Books.AsNoTracking()
              where b.Title.StartsWith("Save")
              select b).FirstOrDefault();

// 您还可以配置ChangeTracker的默认跟踪行为
_booksContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

// 全局配置,还可以使用SQL Server配置跟踪行为
options.UseSqlServer(ConnectionString).UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)

//配置完毕,对数据库进行两次查询,实现两个对象,并且状态信息为空。

当context仅用于读取记录但不进行更改时,使用NoTracking配置非常有用。 这减少了context 的开销,因为不保留状态信息。

更新对象

示例更新对象,并跟踪状态。

public void UpdateRecords()
{
    Book book = _booksContext.Books.Skip(1).FirstOrDefault();
    ShowState();
    book.Title += "UpdateRecords";
    ShowState();
    int records = _booksContext.SaveChanges();
    Console.WriteLine($"{records} updated");
    ShowState();
}
type: Book,state: Unchanged, Id:2,Title:Book
type: Book,state: Modified, Id:2,Title:BookUpdateRecords
1 updated
type: Book,state: Unchanged, Id:2,Title:BookUpdateRecords

从更改跟踪器(change tracker)访问实体时,默认情况下会自动检测更改。ChangeTracker.AutoDetectChangesEnabled 可以开关。 调用DetectChanges可以手动检查是否已完成更改。调用SaveChangesAsync,状态将更改回Unchanged。可以通过调用AcceptAllChanges方法手动执行此操作。

更新未跟踪的对象

DB context通常具有短暂的生命周期。将EF Core与ASP.NET Core MVC一起使用,通过HTTP请求创建context来检索对象(with one HTTP request one object context is created to retrieve objects)。此对象没有和context关联。要在数据库更新它,此对象需要和DB context关联,并且状态会更改以便创建INSERT,UPDATE或DELETE语句。

/// <summary>
/// 更新未跟踪的对象
/// </summary>
public void ChangeUntracked()
{
    Book GetBook()
    {
        return _booksContext.Books.Skip(2).FirstOrDefault();
    }

    Book b = GetBook();
    b.Title += "ChangeUntracked";
    UpdateUntracked(b);

}
/// <summary>
/// 更新未跟踪的对象
/// </summary>
/// <param name="b"></param>
private void UpdateUntracked(Book b)
{
    ShowState();
    // UpdateUntracked方法接收更新的对象,将其与context关联
    // 第一种方式Attach对象,并设置EntityState
    // EntityEntry<Book> entity = _booksContext.Books.Attach(b);
    // entity.State = EntityState.Modified;
    // 使用Update可以自动完成以上注释的语句
    _booksContext.Books.Update(b);
    ShowState();
    _booksContext.SaveChanges();
}
type: AdultBook,state: Modified, Id:3,Title:AdultBookUpdateRecordsChangeUntracked

批处理

停用批处理:MaxBatchSize设置为1

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
	base.OnConfiguring(optionsBuilder);
	optionsBuilder.UseSqlServer(ConnectionString,
	options => options.MaxBatchSize(1));
}

批处理示例:

/// <summary>
/// 创建了100个Book对象,并写入数据库
/// </summary>
public void AddHundredRecords()
{
   
    var books = Enumerable.Range(1, 100).Select(x =>
     new Book
     {
         Title = "AddHundredRecordsssss",
     });
    _booksContext.Books.AddRange(books);
    Stopwatch stopwatch = Stopwatch.StartNew();
    int records = _booksContext.SaveChanges();
    stopwatch.Stop();
    Console.WriteLine($"{records} records added after " + $"{stopwatch.ElapsedMilliseconds} milliseconds");
}

迁移-Need

EF Core 迁移并无多大帮助:在使用EF Core的许多场景中,数据库已经存在。对数据库的更新与应用程序无关,应用程序在数据库更改完成后更新。
EF Core 迁移会有帮助:如果使用应用程序创建数据库。在更改Model时,数据库会自动更新。客户各自拥有数据库,使用新版本应用程序更新老版本应用程序的数据库是一个挑战。EF Core迁移可以解决此问题:通过迁移,您可以轻松地从版本x升级到版本y。从数据库中读取当前版本的数据库,MIGRATIONS具有升级到最新版本的每个步骤所需的信息。 您还可以升级或降级到特定版本。

您有不同的选项来升级数据库。 可以使用升级命令(upgrade commands)直接从应用程序进行迁移。 您还可以使用dotnet命令从命令行更新数据库。 另一种选择是创建一个SQL服务器脚本,数据库管理员可以使用该脚本来更新数据库。

The sample application to show migrations exists from a .NET Standard library and a .NET Core Web application. Typically, data access code is implemented within a library, and there are some additional command-line options needed to deal with this, that’s why migrations are demonstrated in such a scenario.

Deleting Records

诸如EF Core之类的对象关系映射工具不是对所有场景都有好用。 使用示例代码无法有效地删除所有对象。 可以使用一个SQL语句删除所有记录,而不是每个记录一个一个删除。

Logging

要查看发送到数据库的SQL语句,可以使用SQL Server打开探查器,在Visual Studio中打开Intellitrace事件,这需要Visual Studio的企业版,或者只启用日志记录。通过日志记录,您可以在任意位置编写跟踪信息。
EF Core内部使用依赖注入容器(使用Microsoft.Extensions.DependencyInjection),该容器具有注册的ILoggerFactory接口。可以访问此界面并注册自己的Logging provider。

猜你喜欢

转载自blog.csdn.net/Star_Inori/article/details/82192662