EntityFrameWork Core从零开始,(二)一对一实体引用类型的映射

本文详细介绍了在EFCore中配置一对一关联实体的方法,包括默认约定和FluentAPI两种方式,并通过代码示例展示了如何创建数据库表结构。此外,文章还探讨了单向一对一关联的限制,指出EFCore实现一对一关系是通过在外键上设置唯一索引。最后,作者对比了EFCore与JPA的使用体验。
摘要由CSDN通过智能技术生成

一.一对一关联实体的配置(单双向的关联)

前言:说到关联,这正是所有持久层框架的精华,
对实体关联关系配置的复杂与否很大程度上决定了该持久层框架的受欢迎程度

一对一的关联先可以按关联的方法分为

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中的配置,
这也是为数不多我决定写一个系列下去的文章,希望可以一直的更新下去.
谢谢观看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗马苏丹默罕默德

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值