EF7创建模型入门篇

时间:2020-12-27 00:41:36

在EF7中,创建一个模型是非常重要的步骤。本文将使用微软官方文档中的指南,来学习EF7中的创建模型篇,外加一点点个人理解。

实体类型

在 EF7 中,你需要使用 modelBuilder.Entity() 方法来告诉 EF7 你要包含哪些类型。默认情况下,EF7 会将实体类型的名称设置为表的名称。但是,你可以使用 ToTable() 方法来覆盖默认行为。
如果你的数据库中有多个模式(schema),你可以使用 ToTable() 方法的另一个重载版本来指定表所属的架构。如果你想要为生成的表添加注释,可以使用 HasComment() 方法。如果你不想将某个类映射到数据库中的表。我们可以使用 modelBuilder.Entity().Ignore() 来排除它。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
		.ToTable("Blogs", schema: "dbo") //.ToTable("Blogs");
		.HasComment("This table contains blog posts.");
	modelBuilder.Ignore<Address>();
}

共享类型实体类型

在 EF7 中,你可以将一个类型映射到多个表中。这种情况通常发生在你有一组具有相似属性的类型,这些属性在不同的表中都需要使用。在这种情况下,你可以使用 ModelBuilder.SharedTypeEntity() 方法来创建一个实体类型,并将其映射到多个表中。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var addressEntity = modelBuilder.SharedTypeEntity<Address>("Address");
    addressEntity.ToTable("CustomerBillingAddresses");
    addressEntity.ToTable("CustomerShippingAddresses");

    modelBuilder.Entity<Customer>()
        .OwnsOne(c => c.BillingAddress, b =>
        {
            b.WithOwner().HasForeignKey("BillingAddressId");
            b.ToTable("CustomerBillingAddresses");
        });

    modelBuilder.Entity<Customer>()
        .OwnsOne(c => c.ShippingAddress, b =>
        {
            b.WithOwner().HasForeignKey("ShippingAddressId");
            b.ToTable("CustomerShippingAddresses");
        });
}

在上面的代码中,我们首先使用 ModelBuilder.SharedTypeEntity() 方法创建一个名为 Address 的实体类型。然后,我们使用 ToTable() 方法将该实体类型映射到多个表中。接下来,我们使用 OwnsOne() 方法来将 BillingAddressShippingAddress 属性映射到具有相应名称的表中。注意,我们还使用了 HasForeignKey() 方法来指定外键的名称。
使用共享类型实体类型可以使你的代码更加简洁,并提高可维护性。通过使用共享类型实体类型,你可以将一个类型映射到多个表中,而不必在每个实体类型中都定义相同的映射代码。

实体属性

如果要排除实体属性,可以使用Ignore()方法。

modelBuilder.Entity<Person>().Ignore(p => p.Age)

定义列名。例如,下面的代码将为Person类中的LastName属性定义列名。

modelBuilder.Entity<Person>().Property(p => p.LastName).HasColumnName("Last_Name")

定义列注释。例如,下面的代码将为Person类中的LastName属性定义注释。

modelBuilder.Entity<Person>().Property(p => p.LastName).HasComment("The last name of the person")

定义列排序规则。例如,下面的代码将为Person类中的LastName属性定义排序规则。

modelBuilder.Entity<Person>().Property(p => p.LastName).UseCollation("SQL_Latin1_General_CP1_CI_AS");

定义列的数据类型(和数据库一致即可)。例如,下面的代码将为Person类中的Age属性定义为int类型:

modelBuilder.Entity<Person>().Property(p => p.Age).HasColumnType("int");

定义列的最大长度。例如,下面的代码将为Person类中的FirstName属性定义为50个字符的最大长度:

modelBuilder.Entity<Person>().Property(p => p.FirstName).HasMaxLength(50);

定义列的精度和小数位数。例如,下面的代码将为Person类中的Height属性定义为2位小数的精度:

modelBuilder.Entity<Person>().Property(p => p.Height).HasPrecision(5, 2);

定义是否为必需或可选属性。例如,下面的代码将为Person类中的FirstName属性定义为必需属性:

modelBuilder.Entity<Person>().Property(p => p.FirstName).IsRequired();

定义列在表中的顺序。例如,下面的代码将为Person类中的FirstName属性定义为表中第二个列:

modelBuilder.Entity<Person>().Property(p => p.Id).HasColumnOrder(1);
modelBuilder.Entity<Person>().Property(p => p.FirstName).HasColumnOrder(2);

主键

定义主键。根据约定,名为 Id 或 <类型名称>Id 的属性将被配置为实体的主键。

internal class User
{
    public string Id { get; set; } // 主键
    public string Name { get; set; }
}

如果我们不想使用默认约定规则,可以自定义规则。下面的代码将指定User类的Id属性作为主键并且重写设置主键的名称:

modelBuilder.Entity<Person>().HasKey(p => p.Id).HasName("UserId");
注意主键Id应是有序的

在 MySQL 中,主键 Id 不是有顺序的时候,可能会导致新增性能下降的原因是,MySQL 默认使用 B-tree 索引来实现主键索引,如果主键 Id 是无序的,那么在插入数据时,MySQL 需要不断寻找合适的位置来插入新数据,这可能会导致 B-tree 索引不断被调整,从而影响插入性能。相反,如果主键 Id 是有序的,MySQL 可以更快速地找到要插入的位置,从而提高插入性能。

注意主键Id应该是最后生成的

有些程序可能会有延迟,导致数据库插入是非有序的。场景:假如我们先生成Id再处理业务逻辑。有两个线程,同时并发请求。第一个线程生成好Id 是 3,处理下面业务逻辑时发生了大概五毫秒的延迟。第二个线程也生成好了Id是 4,处理下面业务逻辑时没有延迟,就通过了。所以第二个线程,先进数据库插入Id为4。第一个线程因为有延迟来慢了一步,插入的Id是3。数据库Id就变成无序的了。

使用基于时间戳的有序Guid作为主键

对于非复合数字和 GUID 主键,EF Core 根据约定设置值生成。

EF7中 Guid 是基于时间的算法精确到纳秒。因为一毫秒等于一百万纳秒,所以EF7的Guid一毫秒可以产生一百万的Id。
优点:不可预测、有序(添加性能高)、在支持Guid(uuid)的数据库中(查询性能高)、开箱即用。
缺点:(这是可以忽略不计的事情)并发中在同一纳秒内,产生的Id是会重复的。有时钟回拨问题。太长。
EF7中的Guid有序算法比雪花算法更好。

  1. 雪花算法在并发时,也会重复。因为序列号和时间戳,即使我们配置正确了WorkId。不信你可以写个例子,思路是:10个并发同时生成Id,保存到安全线程字典中。重复就报个错。
  2. 雪花算法需要额外维护WorkId的工作。
  3. 有时钟回拨问题。

Guid生成源码地址:MySqlSequentialGuidValueGenerator.cs SqlServerSequentialGuidValueGenerator.cs
使用方式:EF7中:创建模型,生成的值。(***后面会继续讲到)

复合键

复合键是指将多个列作为主键的一种设计模式,这些列在组合起来时才能唯一标识一条记录。相对于单一键,复合键更加灵活,可以更准确地描述实体之间的关系。例如,在一个订单系统中,一个订单可能由多个产品组成,此时可以使用复合键来标识订单编号和产品编号的组合,以确保每个订单中的每个产品都是唯一的。
使用复合键的优点主要有两点。首先,它提供了更准确的数据描述,特别是在处理多对多关系时,可以更准确地表示关系的唯一性。其次,使用复合键可以提高查询效率,因为复合键可以利用数据库的索引机制,快速定位和访问数据。
然而,复合键也存在一些缺点。首先,它增加了开发的复杂性,需要更多的设计和规划。其次,在使用ORM框架时,如EF7,复合键的使用需要特殊的处理,例如在Fluent API中进行配置。最后,复合键在某些情况下可能会导致性能问题,例如在大型数据库中,使用复合键可能会影响查询性能。
在使用EF7时,可以通过Fluent API来配置复合键。以一个用户角色表为例,可以使用以下代码定义一个由用户Id和角色Id的复合主键:

internal class UserRole
{
    public string UserId { get; set; } 
    public string RoleId { get; set; }
}
public class MyContext : DbContext
{
    public DbSet<Person> People { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<UserRole>().HasKey(c => new { c.UserId, c.RoleId });
    }
}

在这个示例中,使用Fluent API中的HasKey方法来定义复合主键。由于复合键是由多个属性组成的,因此需要将它们放在一个匿名类型中作为参数传递给HasKey方法。

注意合理的复合键会提高性能。

MySql为例:当使用复合键并且确保关联数据都已经存在时,插入新数据时可能不会对性能产生太大影响。这是因为在 MySQL 中,使用复合键时,MySQL 会同时使用所有列生成 B-tree 索引,从而提高查询和插入的性能。在插入数据时,如果已经确保了关联数据的存在,那么 MySQL 可以更快速地插入新数据并生成新的索引,从而提高插入性能。
但是,需要注意,如果你的表结构复杂,或者在插入数据时没有正确使用索引,那么复合键仍然可能会影响插入性能。

备选键

为什么要使用EF7备选键?
在数据库中,主键通常用于唯一标识和检索实体对象。但是,有些情况下可能需要使用备选键来标识实体对象。例如,当主键不适合用作某些查询时,使用备选键可以提高数据库的性能和灵活性。
优点:

  1. 提高性能:使用备选键可以减少复杂查询中的连接数量和查询时间,从而提高数据库的性能。
  2. 增强灵活性:备选键允许使用其他属性作为查询条件,这样就可以更灵活地查询数据库中的实体对象。
  3. 减少冲突:使用备选键可以避免主键冲突的情况,尤其是在多个实体对象使用同一主键的情况下。

缺点:

  1. 增加复杂性:使用备选键会增加代码的复杂性,因为需要额外的配置和代码来实现备选键的功能。
  2. 增加维护成本:使用备选键会增加数据库的维护成本,因为需要更多的索引和查询,以及更多的代码来处理备选键。
  3. 影响数据完整性:如果备选键没有正确配置,可能会导致数据完整性的问题,因为重复的备选键可能会导致数据重复或丢失。

为什么有这些优点和缺点?
优点是因为备选键可以提供更灵活、更高效的查询和更少的主键冲突。缺点是因为使用备选键需要更多的配置和代码,并且可能会影响数据完整性。
以下是一个简单的示例,演示如何使用备选键:

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

public class MyContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
		// 将Email属性指定为备选键
        modelBuilder.Entity<Person>().HasAlternateKey(p => p.Email); 
    }
}

复合备选键

为什么要使用复合备选键?
使用复合备选键的一个重要原因是在某些情况下,单个列可能不足以唯一标识表中的每一行。例如,在一个电影数据库中,如果只使用电影名称作为主键,则会出现多个电影名称相同的情况。因此,需要使用复合备选键,通过多个列来确定每个电影的唯一性。
优点

  1. 更加灵活的数据建模:复合备选键使得数据建模更加灵活,可以在表中使用多个列来确定唯一性。这使得数据建模更加符合实际情况,可以更好地支持复杂的业务场景。
  2. 更好的性能:使用复合备选键可以提高数据库的性能。这是因为使用多个列来唯一标识行,可以减少在表中的扫描次数,从而提高查询性能。
  3. 更好的数据完整性:使用复合备选键可以更好地保证数据完整性。在使用复合备选键的表中,每个行的唯一性都由多个列决定,这使得在数据插入和更新时,更难发生数据冲突和错误。

缺点

  1. 复杂性:使用复合备选键会增加表的复杂性。需要在表中定义多个列,以确定唯一性。此外,在查询时,需要指定多个列作为条件,以获取唯一的行。这可能会增加代码的复杂性,需要更加谨慎地编写代码。
  2. 不支持自动增长:使用复合备选键时,不支持自动增长功能。这是因为每个行的唯一性都是由多个列来决定的,如果一个列是自动增长的,就不能保证每一行都是唯一的。

使用方式
使用Fluent API是定义复合备选键的最佳方式。以下是使用Fluent API定义复合备选键的示例:

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
}

public class PersonContext : DbContext
{
    public DbSet<Person> Persons { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>()
			// 将多个属性配置为备选键(即复合备选键)
			.HasAlternateKey(p => new { p.FirstName, p.LastName, p.DateOfBirth })
			// 可配置备选键的索引和唯一约束的名称:
			.HasName("FirstName_LastName_DateOfBirth");
    }
}

分组配置模型

可以使用分组配置,这样可以将实体和关系的配置组织在一起,使代码更具可读性。例如:

public class BlogConfiguration : IEntityTypeConfiguration<Blog>
{
    public void Configure(EntityTypeBuilder<Blog> builder)
    {
        builder.HasKey(x => x.BlogId);
        builder.Property(x => x.Name).IsRequired();
    }
}

然后在DbContext中使用以下代码将此配置应用于Blog实体:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new BlogConfiguration());
}

使用数据注释来配置模型

可以使用数据注释来配置EF7模型。数据注释是一种以属性和类注释的方式来提供元数据的方法。例如:

public class Blog
{
    [Key]
    public int BlogId { get; set; }
    [Required]
    public string Name { get; set; }
}

在这个示例中,我们使用了Key和Required注释来指定BlogId和Name属性的主键和IsRequired标志。

内置约定

除了手动配置外,EF7还提供了一些内置约定,可以根据惯例自动推断模型。例如,如果实体中有一个名为Id的属性,EF7会将其作为主键。如果实体之间具有引用关系,EF7会自动创建外键。

删除现有约定

如果不想使用内置约定,可以通过调用ModelBuilder.Conventions.Remove方法来删除它们。例如,以下代码删除了为外键列创建索引的约定:

modelBuilder.Conventions.Remove<ForeignKeyIndexConvention>();

总结:

在本文中,介绍了如何告诉EF7使用实体类型和过滤类型,并且还说了共享实体类型。我们还介绍了实体属性的配置。还介绍了四种键。介绍了使用Fluent API和数据注释来配置EF7模型。我们还了解了EF7的内置约定,并学习了如何删除现有约定。使用这些技术,可以轻松地创建和配置EF7模型,并更好地管理数据库访问代码。