一.一对一关联实体的配置(单双向的关联)
前言:说到关联,这正是所有持久层框架的精华,
对实体关联关系配置的复杂与否很大程度上决定了该持久层框架的受欢迎程度
一对一的关联先可以按关联的方法分为
1. 一对一单项关联
2. 一对一双向关联
(PS.在官方文档中,它们称为是否能从一个属性导航到另外一个对象)
又可以更具数据库的主外键设计分为
1. 一对一主键关联(主键同时作为外键)
2. 一对一外键关联(外键手动设置为Unique属性)
一点小复习
当然,在C#中没那么复杂,它只有两种方式对此进行配置
1.默认推荐的模式(不使用注解[标记],也不使用OnModelCreate函数)
参考了该网站的内容:https://www.entityframeworktutorial.net/efcore/one-to-one-conventions-entity-framework-core.aspx,该模式大致为一种EFCore默认的1to1的默认约定
以经典的身份证和人的关系为例
namespace EFCoreRelationTest.OneToOne
{ //Person 1 - 1 idcard
class Person
{
public int id { get; set; }
public string name { get; set; }
public string address { get; set; }
//加入引用属性
public IdentityCard idcard { get; set; }
public override string ToString()
{
return "id:" + id + "name" + name + "address:" + address;
}
}
}
namespace EFCoreRelationTest.OneToOne
{
//身份证类
class IdentityCard
{
public int ID { get; set; }
public DateTime birth { get; set; }
//使用与引用对象关联的属性作为标识
//一般命名是引用类名+id,类型要与引用类的id属性相同
//在这里就是和Person类的id属性int相同
public int PersonID { get; set; }
public Person person { get; set; }
public override string ToString()
{
return "身份證號:" + ID + "出生日期: " + birth + "主人信息" + person;
}
}
}
在DbContext中并不需要再做什么修改
//OneToOne關係的數據庫上下文
namespace EFCoreRelationTest.OneToOne
{
//这种不需要修改任何DbContext中的代码
class OTODbcontext:DbContext
{
public DbSet<Person> people { get; set; }
public DbSet<IdentityCard> cards { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var Connection = "server=.;Database=EFCoreTest;uid=XXXXXXX;pwd=XXXXXX";
optionsBuilder.UseSqlServer(Connection);
}
}
}
在包管理控制台中输入
Add-Migration initialcreate
build成功后再输入
Update-Database
都提示成功后可以查看EFCore生产的迁移和建表的文件
这里需要分析一下这个建表的文件的内容
namespace EFCoreRelationTest.Migrations
{
//数据库上下文标记和一个建表的时间戳
[DbContext(typeof(OTODbcontext))]
[Migration("20211129021022_initialcreate")]
partial class initialcreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("ProductVersion", "5.0.12")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
//可以看到这个全限定名,说明这里生成的是身份证类
modelBuilder.Entity("EFCoreRelationTest.OneToOne.IdentityCard", b =>
{
//一般会将int类型id,ID,%id什么的属性映射为自增主键
//即 ID int primary key identity(1,1)
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<int>("PersonID")
.HasColumnType("int");
b.Property<DateTime>("birth")
.HasColumnType("datetime2");
//此处设置了主键
b.HasKey("ID");
//注意: 在这里设置了一个PersonID的唯一索引,这是OneToOne的限制
b.HasIndex("PersonID")
.IsUnique();
//以cards为表名建表
b.ToTable("cards");
});
//在这个地方设置了Person类的普通属性
modelBuilder.Entity("EFCoreRelationTest.OneToOne.Person", b =>
{
b.Property<int>("id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("address")
.HasColumnType("nvarchar(max)");
b.Property<string>("name")
.HasColumnType("nvarchar(max)");
b.HasKey("id");
b.ToTable("people");
});
//注意:在此处绑定了Person和IdentityCard的一对一的关系
//我精良解释一下下面语句的作用
modelBuilder.Entity("EFCoreRelationTest.OneToOne.IdentityCard", b =>
{
//B类中有一个引用属性,为person并属于该限定名
b.HasOne("EFCoreRelationTest.OneToOne.Person", "person")
//引用的Person类中也有一个本类的引用,名为idcard
.WithOne("idcard")
//设置PersonID属性为外键参照引用类的ID属性
.HasForeignKey("EFCoreRelationTest.OneToOne.IdentityCard", "PersonID")
//设置了删除的级联设置
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
//设置导航属性person
b.Navigation("person");
});
modelBuilder.Entity("EFCoreRelationTest.OneToOne.Person", b =>
{
b.Navigation("idcard");
});
#pragma warning restore 612, 618
}
}
}
[数据库设计图]
现在可以先添加两条数据,然后在进行测试
class Program
{
static void Main(string[] args)
{
/*一對一關係測試,EFCore默認的一對一關係配置*/
OTODbcontext dbcontext = new OTODbcontext();
Person zhangsan = new Person();
zhangsan.name = "張三";
zhangsan.address = "新京SD";
IdentityCard zhangsanCard = new IdentityCard();
zhangsanCard.person = zhangsan;
zhangsanCard.birth = DateTime.Parse("2000-11-23");
zhangsan.idcard = zhangsanCard;
dbcontext.people.Add(zhangsan);
dbcontext.cards.Add(zhangsanCard);
dbcontext.SaveChanges();
Person p1 = dbcontext.Find<Person>(1);
IdentityCard id = dbcontext.Find<IdentityCard>(1);
Console.WriteLine(id);
}
}
正常运行:
身份證號:1出生日期: 2000/11/23 00:00:00主人信息id:1name張三address:新京SD
强行改变OneToOne关联属性试一试
OTODbcontext dbcontext = new OTODbcontext();
IdentityCard theTwo = new IdentityCard();
theTwo.birth = DateTime.Parse("2021-11-29");
theTwo.person = dbcontext.Find<Person>(1);
dbcontext.cards.Add(theTwo);
dbcontext.SaveChanges();
Console.WriteLine(dbcontext.Find<IdentityCard>(2));
[會出現唯一索引的報錯提示,說明EFCore並不是通过给外键加上Unique来实现的一对一而是通过唯一索引的方式来实现]
2.FluentAPI配置一对一关系
FluentAPI是在OnCreateModeling函数中使用的API,也是官方很推崇的一种配置和设置模型的一种方式,
让我们再写一个例子来完成这个演示:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public StudentAddress Address { get; set; }
}
public class StudentAddress
{
public int StudentAddressId { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Country { get; set; }
//这种方式依旧需要这个辅助的id属性,因为要用它来生成外键
public int AddressOfStudentId { get; set; }
public Student Student { get; set; }
DbContext:
public class SchoolContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//使用FluentAPI配置一对一关系
//使用HasOne配置Entity方法泛型的引用类型
//WithOne则指定引用类型中的泛型成员,
//最后用HasForeignKey指定那个外键属性生成外键关联
modelBuilder.Entity<Student>()
.HasOne<StudentAddress>(s => s.Address)
.WithOne(ad => ad.Student)
.HasForeignKey<StudentAddress>(ad => ad.AddressOfStudentId);
}
public DbSet<Student> Students { get; set; }
public DbSet<StudentAddress> StudentAddresses { get; set; }
}
当然只要EFCore能判断出外键属性是与引用对象的关系,(即向上面默认的方式一样)
则根本不用使用这个FluentAPI
分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线
但如果你不想要定义这个这个外键属性,觉得如此设计不够优雅,这当然也是可以的,
此时你需要使用[ForeignKey(String name)]
再写一个例子:
namespace EFCoreRelationTest.OneToOne
{
class Student
{
public int id { get; set; }
public string name { get; set; }
public int age { get; set; }
public string classandgrade { get; set; }
public StudentAddress SA { get; set; }
}
class StudentAddress
{
public int id { get; set; }
public string address { get; set; }
//如果不设置外键属性,只需要一个Foreignkey标注即可
[ForeignKey("t_studentAddress")]
public Student student { get; set; }
}
}
FluentAPI
class SASDbContext:DbContext
{
public DbSet<Student> students { get; set; }
public DbSet<StudentAddress> addresses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var Connection = "XXXXXXXXXXXXXXXXXXXXXXXXXXX";
optionsBuilder.UseSqlServer(Connection);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>().HasOne<StudentAddress>(student => student.SA)
.WithOne(StudentAddress => StudentAddress.student);
//当用标记指定了外键之后,就不需要在FluentAPI中指定外键属性了
}
}
然后进入包管理控制台,(由于这个测试项目里已经有多个DbContext了,需要在命令行中指定要migration的DbContext)
再输入一般迁移和建表的命令
如上例中则为
add-migration initialcreate -c SASDbContext
update-database -Context SASDbContext
再去看一看数据库里生成的表,它们的结果与默认的一对一的方式生成的结果一模一样,
所以无需再次测试
3.一对一单向关联的配置
一个错误的示范
如果在單向的一對一中我們只在从表中配置关联,(主表对应的类中没有属性可以导航到从表类)
这样会被EFCore当成是普通的一对多的情况
就像这样:
//单向一对一的错误示范
namespace EFCoreRelationTest.SingleOneToOne
{
class Car
{
public int id { get; set; }
public string owner { get; set; }
public string brand { get; set; }
}
class CarCard
{
public int id { get; set; }
[Required]
[MaxLength(100)]
public string cardnumber { get; set; }
public Car car { get; set; }
}
}
DbContext
//不能只配置一边
class SingleDbContext:DbContext
{
public DbSet<Car> cars { get; set; }
public DbSet<CarCard> t_cards { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("server=.;Database=EFCoreTest;uid=sa;pwd=LiHan199968");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//設置單邊的關係
modelBuilder.Entity<CarCard>().HasOne<Car>(card => card.car);
//設置一個cardnumber屬性為Unique屬性
modelBuilder.Entity<CarCard>().HasIndex(card => card.cardnumber).IsUnique();
}
}
输入刚才提到的控制台指令生成表之后
会为
Carcard类的表(从表)生成一个外键参照于Car类对应表(主表),但此外建并没有别的约束
我测试了一下,确实是可以CarCard对象对应多个Car对象,
结论:此法不行
但是
在上例之中,笔者为上例中的CarCard类设置了一个Unique属性,
大家肯定了解Unique属性,这是比较简单的,
如果读者看到我在文章开始时画的那张图,
有种一对一的数据库实现就是把普通的一对多的外键设置成Unique,
则这样就强行变成一对一了
然而EFCore并不能在引用类型生成的外键上设置为unique,所以需要我们手动改为Unique
(PS:关于让既是主键又是外键的玩法无法在EFCore中设置)
结论
就我今天的研究,EFCore并不支持单向关联的一对一(虽然用的的确比较少)
它的一对一关系的实现只有一种,既是在外键上设置一个唯一索引
私货
笔者比较熟悉Hibernate及JPA和SpringDataJpa的知识,
切来评论一下使用两者的感受
1.相交于JPA,EFCore的标记功能实在是太弱了,
JPA,SJPA能将所有的关系及约束在注释黎写的明明白白
而EFCore连个[Unique]都只能去FluentAPI里写
2.Session和DbContext,DbContext看上去比Sesssion灵活,但对实体的监控(实体的三种状态)并没有Session好,
其次相较于SJPA,直接将EntityManagement(JPA的session)给隐藏了,让我们基本不用写持久层代码,这真是不知道高到哪里去了.
总体说EFCore的使用更加难一些,但是原理则比较简单
下一篇文章将讨论一下一对多/多对一的关联在EFCore中的配置,
这也是为数不多我决定写一个系列下去的文章,希望可以一直的更新下去.
谢谢观看