在EF7中,创建一个模型是非常重要的步骤。本文将使用微软官方文档中的指南,来学习EF7中的创建模型篇,外加一点点个人理解。
实体类型
在 EF7 中,你需要使用 modelBuilder.Entity
如果你的数据库中有多个模式(schema),你可以使用 ToTable() 方法的另一个重载版本来指定表所属的架构。如果你想要为生成的表添加注释,可以使用 HasComment() 方法。如果你不想将某个类映射到数据库中的表。我们可以使用 modelBuilder.Entity
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.ToTable("Blogs", schema: "dbo") //.ToTable("Blogs");
.HasComment("This table contains blog posts.");
modelBuilder.Ignore();
}
共享类型实体类型
在 EF7 中,你可以将一个类型映射到多个表中。这种情况通常发生在你有一组具有相似属性的类型,这些属性在不同的表中都需要使用。在这种情况下,你可以使用 ModelBuilder.SharedTypeEntity() 方法来创建一个实体类型,并将其映射到多个表中。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var addressEntity = modelBuilder.SharedTypeEntity("Address");
addressEntity.ToTable("CustomerBillingAddresses");
addressEntity.ToTable("CustomerShippingAddresses");
modelBuilder.Entity()
.OwnsOne(c => c.BillingAddress, b =>
{
b.WithOwner().HasForeignKey("BillingAddressId");
b.ToTable("CustomerBillingAddresses");
});
modelBuilder.Entity()
.OwnsOne(c => c.ShippingAddress, b =>
{
b.WithOwner().HasForeignKey("ShippingAddressId");
b.ToTable("CustomerShippingAddresses");
});
}
在上面的代码中,我们首先使用 ModelBuilder.SharedTypeEntity() 方法创建一个名为 Address 的实体类型。然后,我们使用 ToTable() 方法将该实体类型映射到多个表中。接下来,我们使用 OwnsOne() 方法来将 BillingAddress 和 ShippingAddress 属性映射到具有相应名称的表中。注意,我们还使用了 HasForeignKey() 方法来指定外键的名称。
使用共享类型实体类型可以使你的代码更加简洁,并提高可维护性。通过使用共享类型实体类型,你可以将一个类型映射到多个表中,而不必在每个实体类型中都定义相同的映射代码。
实体属性
如果要排除实体属性,可以使用Ignore()方法。
modelBuilder.Entity().Ignore(p => p.Age)
定义列名。例如,下面的代码将为Person类中的LastName属性定义列名。
modelBuilder.Entity().Property(p => p.LastName).HasColumnName("Last_Name")
定义列注释。例如,下面的代码将为Person类中的LastName属性定义注释。
modelBuilder.Entity().Property(p => p.LastName).HasComment("The last name of the person")
定义列排序规则。例如,下面的代码将为Person类中的LastName属性定义排序规则。
modelBuilder.Entity().Property(p => p.LastName).UseCollation("SQL_Latin1_General_CP1_CI_AS");
定义列的数据类型(和数据库一致即可)。例如,下面的代码将为Person类中的Age属性定义为int类型:
modelBuilder.Entity().Property(p => p.Age).HasColumnType("int");
定义列的最大长度。例如,下面的代码将为Person类中的FirstName属性定义为50个字符的最大长度:
modelBuilder.Entity().Property(p => p.FirstName).HasMaxLength(50);
定义列的精度和小数位数。例如,下面的代码将为Person类中的Height属性定义为2位小数的精度:
modelBuilder.Entity().Property(p => p.Height).HasPrecision(5, 2);
定义是否为必需或可选属性。例如,下面的代码将为Person类中的FirstName属性定义为必需属性:
modelBuilder.Entity().Property(p => p.FirstName).IsRequired();
定义列在表中的顺序。例如,下面的代码将为Person类中的FirstName属性定义为表中第二个列:
modelBuilder.Entity().Property(p => p.Id).HasColumnOrder(1);
modelBuilder.Entity().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().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有序算法比雪花算法更好。
- 雪花算法在并发时,也会重复。因为序列号和时间戳,即使我们配置正确了WorkId。不信你可以写个例子,思路是:10个并发同时生成Id,保存到安全线程字典中。重复就报个错。
- 雪花算法需要额外维护WorkId的工作。
- 有时钟回拨问题。
使用方式见这篇文章:《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().HasKey(c => new { c.UserId, c.RoleId });
}
}
在这个示例中,使用Fluent API中的HasKey方法来定义复合主键。由于复合键是由多个属性组成的,因此需要将它们放在一个匿名类型中作为参数传递给HasKey方法。
注意合理的复合键会提高性能。
MySql为例:当使用复合键并且确保关联数据都已经存在时,插入新数据时可能不会对性能产生太大影响。这是因为在 MySQL 中,使用复合键时,MySQL 会同时使用所有列生成 B-tree 索引,从而提高查询和插入的性能。在插入数据时,如果已经确保了关联数据的存在,那么 MySQL 可以更快速地插入新数据并生成新的索引,从而提高插入性能。
但是,需要注意,如果你的表结构复杂,或者在插入数据时没有正确使用索引,那么复合键仍然可能会影响插入性能。
备选键
为什么要使用EF7备选键?
在数据库中,主键通常用于唯一标识和检索实体对象。但是,有些情况下可能需要使用备选键来标识实体对象。例如,当主键不适合用作某些查询时,使用备选键可以提高数据库的性能和灵活性。
优点:
- 提高性能:使用备选键可以减少复杂查询中的连接数量和查询时间,从而提高数据库的性能。
- 增强灵活性:备选键允许使用其他属性作为查询条件,这样就可以更灵活地查询数据库中的实体对象。
- 减少冲突:使用备选键可以避免主键冲突的情况,尤其是在多个实体对象使用同一主键的情况下。
缺点:
- 增加复杂性:使用备选键会增加代码的复杂性,因为需要额外的配置和代码来实现备选键的功能。
- 增加维护成本:使用备选键会增加数据库的维护成本,因为需要更多的索引和查询,以及更多的代码来处理备选键。
- 影响数据完整性:如果备选键没有正确配置,可能会导致数据完整性的问题,因为重复的备选键可能会导致数据重复或丢失。
为什么有这些优点和缺点?
优点是因为备选键可以提供更灵活、更高效的查询和更少的主键冲突。缺点是因为使用备选键需要更多的配置和代码,并且可能会影响数据完整性。
以下是一个简单的示例,演示如何使用备选键:
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().HasAlternateKey(p => p.Email);
}
}
复合备选键
为什么要使用复合备选键?
使用复合备选键的一个重要原因是在某些情况下,单个列可能不足以唯一标识表中的每一行。例如,在一个电影数据库中,如果只使用电影名称作为主键,则会出现多个电影名称相同的情况。因此,需要使用复合备选键,通过多个列来确定每个电影的唯一性。
优点
- 更加灵活的数据建模:复合备选键使得数据建模更加灵活,可以在表中使用多个列来确定唯一性。这使得数据建模更加符合实际情况,可以更好地支持复杂的业务场景。
- 更好的性能:使用复合备选键可以提高数据库的性能。这是因为使用多个列来唯一标识行,可以减少在表中的扫描次数,从而提高查询性能。
- 更好的数据完整性:使用复合备选键可以更好地保证数据完整性。在使用复合备选键的表中,每个行的唯一性都由多个列决定,这使得在数据插入和更新时,更难发生数据冲突和错误。
缺点
- 复杂性:使用复合备选键会增加表的复杂性。需要在表中定义多个列,以确定唯一性。此外,在查询时,需要指定多个列作为条件,以获取唯一的行。这可能会增加代码的复杂性,需要更加谨慎地编写代码。
- 不支持自动增长:使用复合备选键时,不支持自动增长功能。这是因为每个行的唯一性都是由多个列来决定的,如果一个列是自动增长的,就不能保证每一行都是唯一的。
使用方式
使用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()
// 将多个属性配置为备选键(即复合备选键)
.HasAlternateKey(p => new { p.FirstName, p.LastName, p.DateOfBirth })
// 可配置备选键的索引和唯一约束的名称:
.HasName("FirstName_LastName_DateOfBirth");
}
}
分组配置模型
可以使用分组配置,这样可以将实体和关系的配置组织在一起,使代码更具可读性。例如:
public class BlogConfiguration : IEntityTypeConfiguration<Blog>
{
public void Configure(EntityTypeBuilder 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();
总结:
在本文中,介绍了如何告诉EF7使用实体类型和过滤类型,并且还说了共享实体类型。我们还介绍了实体属性的配置。还介绍了四种键。介绍了使用Fluent API和数据注释来配置EF7模型。我们还了解了EF7的内置约定,并学习了如何删除现有约定。使用这些技术,可以轻松地创建和配置EF7模型,并更好地管理数据库访问代码。