约定,类似于接口,是一个规范和规则,使用Code First 定义约定来配置模型和规则。在这里约定只是记本规则,我们可以通过Data Annotaion或者Fluent API来进一步配置模型。约定的形式有如下几种:
- 类型发现约定
- 主键约定
- 关系约定
- 复杂类型约定
- 自定义约定
零、类型发现约定
在Code First 中。我们定义完模型,还需要让EF上下文你知道应该映射那些模型,此时我们需要通过 DbSet 属性来暴露模型的。如果我们定义的模型由继承层次,只需要为基类定义一个DbSet属性即可(如果派生类与基类在同一个程序集,派生类将会被自动包含),代码如下:
public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }
public virtual ICollection Blogs { get; set; }
}
public class EfDbContext : DbContext
{
public EfDbContext()
{
}
public DbSet Departments { get; set; }
}
当然,有时候我们不希望模型映射到数据库中,这时我们可以通过Fluent API 来忽略指定的模型映射到数据库中,代码写在EF上下文中:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Ignore();
}
一、主键约定
Code First 会根据模型中定义的id,推断属性为主键(如果类中没有id属性,会查找定义成类名称+id的属性,将这个属性作为主键)。如果主键类型是int 或者 guid 类型,主键将会被映射为自增长标识列。例如我们上一小节中定义的类 Department,类中没有名称为id的属性,但是存在名称为类名称+id的属性DepartmentId,因此DepartmentId属性,将会被映射为自增长的主键。如果一个类中既没有id属性,也没有类名+id的属性,那么代码在运行时将会报错,因为EF没有找到符合要求的字段创建主键。
二、关系约定
在数据库中,我们可以通过多张表的关联查询出数据,这多张表之间的关联,就是他们的关系。同样,也可以在模型中定义这样的关系。EF中定义关系要使用到导航属性,通过导航属性可以定义多个模型之间的关系。大部分情况下我们会将导航属性和外键属性结合在一起使用。导航属性的命名规则如下:导航属性名称+主体主键名称 或者 主体类名+主键属性名称 或者 主体主键属性名。当EF检测出外键属性后,会根据外键属性是否为空来判断关系,如果外键可以为空,那么模型之间的关系将会配置成可选的,Code First 不会再关系上配置级联删除。看一个简单的代码:
public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }
public virtual ICollection Students { get; set; }
}
public class Student
{
public int StudentId { get; set; }
public string Name { get; set; }
public int DepartmentId { get; set; }
public virtual Department Department { get; set; }
}
三、复杂类型约定
在Code First 不能推断出模型中的主键,并且没有通过Data Annotations 或者Fluent API进行手动配置主键时,该模型将会自动被配置为复杂类型,检测复杂类型时要求该类型没有引用实体类型的属性。简单的说就是:一个复杂类型作为已存在对象的属性,EF会将复杂类型的类映射到已存在的表中,已存在的表包将包含这些列,而不是将复杂类型映射成另外单独的一张表。我们来看一下例子:
public class EfDbContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity().ToTable("Orders");
modelBuilder.ComplexType();
}
public DbSet Orders { get; set; }
}
public class Order
{
public int Id;
public string Name;
public class Address
{
public string Street;
public string Region;
public string Country;
}
}
四、自定义约定
当EF提供的默认约定都不符合我们要求的时候,我们可以使用自定义约定。自定义约定可以看作全局约定规则,将会运用到所有实体和属性,也可以显示实现应用到指定的模型上。
如果项目要求模型中有Id属性,就将Id作为主键映射,那么我们有两种选择来定义这个约定,首先我们而已选择Fluent API ,其次我们也可以选择自定义约定。自定义约定相对来说比Fluent API 要简单,只需一行代码即可解决。我们只需要在 OnModelCreating 方法中加入如下代码即可:
modelBuilder.Properties().Where(p => p.Name == "Id").Configure(p => p.IsKey());
注:当多个属性存在相同约定配置时,最后一个约定将覆盖前面所有相同的约定。
自定义约定包含一个约定接口 IConvention,IConceptualModelConvention 是概念模型接口,在模型创建后被调用,IStoreModelConvention 接口为存储模型接口,在模型创建之后用于操作对模型的存储,自定义类约定都必须在 OnModelCreating 方法中显式配置,例如我们要将模型中类型为DateTime的属性映射为datetime2,可进行如下配置:
public class DateTime2Convention : Convention
{
public DateTime2Convention()
{
this.Properties().Configure(c => c.HasColumnType("datetime2"));
}
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Add(new DateTime2Convention());
}
当我们自定义约定需要在另一个约定运行之前或者运行之后执行时,有可能会受到默认原定的影响,这时我们可以用到:AddBefore 和 AddAfter* 方法,例如:将我们前面创建的约定放在内置约定发现逐渐约定之前运行。
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.AddBefore(new DateTime2Convention());
}
在开发过程中都会存在开发规范,例如对表名命名的规则,我们可以调用Types 方法该表表明约定,代码如下:
public string GetTableName(Type type)
{
var result = Regex.Replace(type.Name, ".[A-Z]",m=>m.Value[0]+"_"+m.Value[1]);
return result.ToLower();
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Types().Configure(c => c.ToTable(GetTableName(c.ClrType)));
}
上述我们讲的都是针对全局的约定,我们在开发工程中大部分遇到的是针对符合特定条件的模型进行约定,此时我们就用到了自定义特性。我们先来看一段代码:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NoUnicode : Attribute
{
}
这段代码将类型为字符串的属性配置为非Unicode,下面我们建上面的特性应用到所有模型
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties().Where(x => x.GetCustomAttributes(false).OfType().Any())
.Configure(c => c.IsUnicode(false));
}
添加该特性后,映射在数据库中的列将是 varchar 类型,而不是 nvarchar 类型。但是上述代码存在一个问题,如果匹配的不是字符串类型将会报错,因此我们将代码更新如下:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties().Where(c => c.GetCustomAttributes(false).OfType().Any())
.Configure(c => c.IsUnicode(false));
modelBuilder.Properties().Having(x => x.GetCustomAttributes(false).OfType().FirstOrDefault())
.Configure((config, attr) => config.IsUnicode(attr.Uniconde));
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NoUnicode : Attribute
{
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
internal class IsUnicode : Attribute
{
public bool Uniconde { get; set; }
public IsUnicode(bool isUnicode)
{
Uniconde = isUnicode;
}
}