EntityFramework之一对一关系(二)

前言

关于表关系园中文章也是数不胜收,但是个人觉得最难攻克的是一对一,对其配置并非无道理可循,只要掌握了原理方可,且听我娓娓道来!

共享主键关系

概念:就是两个表共享相同的主键值,也就是说一表的主键值是另外一个表的外键值。

我们现在给出三个类,一个是User(用户类),一个是Address(地址类),最后一个是Shipment(运货车类)。每个用户都对应一个银行账户地址也就是Address,同时运货车都有一个运货的地点也就是Address。鉴于此设计类图如下并且我们建立如下三个类。

    /*用户类*/    public class User
    {
        public int UserId { get; set; }
       public string Name { get; set; }

        public virtual Address BillingAddress { get; set; }
    }

    /*运货车类*/
    public class Shipment
    {
        public int ShipmentId { get; set; }
                public string State { get; set; }

        public virtual Address DeliveryAddress { get; set; }
    }

    /*地点类*/
    public class Address
    {
        public int AddressId { get; set; }
       public string Street { get; set; }
       public string City { get; set; }
       public string ZipCode { get; set; }
    }

 我们通过如下映射来得到一对一的关系:

用户映射类

    public class UserMap:EntityTypeConfiguration<User>
    {
        public UserMap()
        {
            HasOptional(p => p.BillingAddress).WithRequired();
        }
    }

运货车映射类

    public class ShipmentMap : EntityTypeConfiguration<Shipment>
    {
        public ShipmentMap()
        {
            HasRequired(p => p.DeliveryAddress).WithOptional();
        }
    }

【注意】在上述关系中我们无需指定外键,因为当其属性暴露在实体中时我们才用HasForeignKey()方法进行指定, 同时因为EF仅仅支持一对一关系在主键上,所以它将会自动在数据库中在主键上建立关系。 

数据库设计图

下面数据库设计图是基于EF Code First映射的结果

 那么我们如何知道创建的表中谁是主键谁是外键呢?我们通过参照性完整性规则来看看

参照性完整规则

用户表外键关系

运货车表外键关系

 从上述两个外键关系中我们可以看出:EF Code First添加了一个连接地址(Address)的主键到用户(User)主键的外键约束,同时也添加了一个连接运货车(Shipment)的主键到地址(Address)主键的外键约束。也就意味着,地址的主键依据用户主键来定,而运货车主键根据地址的主键来定。

那么问题来了,在关系映射中,EF Code  First最终是怎样决定谁是主体对象谁是依赖对象呢?

不难看出EF Code First是根据你的对象模型来判定的,例如我们上述用一下代码来判定用户和地址之间 的关系

HasOptional(p => p.BillingAddress).WithRequired();

这意思就是用户实体对于地址是可选的关系,但是地址对于用户却是必须的关系,所以我们得出结论:在这种关系中,最终用户将是主体对象而地址最终将是依赖对象。同时通过上述参照完整性规则中的外键关系我们也能得出这样的结论。

不知道你们注意到没在第一幅图关于数据库设计图中,我还做了标记,生成的UserId是标识列,而AddressId和ShipmentId不是,不信让你看看它俩Id的标识,如下图:

依据所给图我们得出对于一对一关系结论:依赖对象的主键默认将不会被标识。

从上我们知道,每一个地址总数属于一个用户,每一个运货车总是对应相应的地址。那么问题又来了,我们是不是只要删除一个用户那么是不是地址和运货车就会相应的进行删除呢? 

 默认情况下,EF Code First是不会进行级联删除的,我们又要保护参照性完整规则,于是我们只能手动通过Fluent API进行级联删除,例如对于用户来说如下:

HasOptional(p => p.BillingAddress).WithRequired().WillCascadeOnDelete();

其他方法如WithOptionalDependent用来做什么的呢?

 HasRequired() 方法返回 RequiredNavigationPropertyConfiguration 对象的类型,在此类中除了我们通常用到的典型的 WithMany() 和WithOptional()  方法外还定义了两个特别的方法 WithRequiredDependent()和WithRequiredPrincipal() 方法,为什么有这两个方法呢?我们知道在EF Code Firs指出了在关系中的主体对象和依赖对象的唯一原因是通过Fluent API能够最终明确指出一个是必须的(Required),另一个是可选的(Optional),但是要是我们在关系中都是必须的或者都是可选的那该怎么办呢?例如在一种场景下一个地址总是对应一个用户,一个用户总是对应一个地址(双方都是必须的),所以在此种情况下,EF Code First就不能明确指出谁是主体对象谁是依赖对象,于是就引入了WithRequiredDependent()方法,简而言之,这种配置最终需要Fluent API来完成(不谈论Data Annotation),而Fluent API就设计了一种方式,这种方式就是强迫你明确指出谁是主体对象谁是依赖对象在两个都是可选或者两个都是必须的条件下。 

例如:综上在User和Address两个都是必须的前提下,我们如下配置即可:

HasRequired(p => p.BillingAddress).WithRequiredDependent();

请看下图,正如我们所分析的,当你需要两者都是必须的时候,是没有WithRequired()方法在此类中:

接下来我们添加数据进行测试:

            EntityDbContext ctx = new EntityDbContext();
            Address billingAddress = new Address()
            {
                Street = "华容道",
                City = "岳阳"
            };

            User user = new User()
            {
                Name = "莫扎特",
                BillingAddress = billingAddress
            };
            ctx.Set<User>().Add(user);
            ctx.SaveChanges(); 

很显然我们无需指定UserId,因为上述已经说明其为标识列即自动增长,并且此时AddressId与UserId相同。

接下来我们添加Address数据和Shipment数据

    EntityDbContext ctx = new EntityDbContext();
    Address deliveryAddress = new Address()
    {
        AddressId = 1,
        Street = "华容道",
    };

    Shipment shipment = new Shipment()
    {
        ShipmentId = 1,
        State = "true",
        DeliveryAddress = deliveryAddress
    };

    ctx.Set<Shipment>().Add(shipment);
    ctx.SaveChanges();

此时运行肯定会报错,因为在第一次添加数据时,AddressId就已经为1,鉴于约束无法为其添加重复值所以无法进行更新!

通过一对于一关系的共享主键有一个最大限制:

很难保存相关对象:因为当对象被保存时要确保相关的实例的被分配的主键值也相同(例如当新添加一个Address时,你得确保要提供唯一的一个AddressId并且这个AddressId能够在User中有相同的这样一个值作为UserId。

概要

通过主键共享只是实现一对一关系的一种方式,鉴于上述实现在实际应用中并不常用可以说是相当罕见,在许多场景下,我们更多实现一对一关系是通过添加一个外键字段和唯一约束,接下来我们将通过外键来实现这种方式将无主键共享方式诸多限制。

外键关系

我们现在对上面类进行改造,现在场景是每个用户对应两个地址,一个是BillingAddress(账户地址),一个是FamlilyAddress(家庭地址)!建立类以及类图如下:

public class User
{
    public int UserId { get; set; }
    public string Name { get; set; }
    public int BillingAddressId { get; set; }
    public int FamlilyAddressId { get; set; }

    public Address BillingAddress { get; set; }
    public Address FamlilyAddress { get; set; }
}

public class Address
{
    public int AddressId { get; set; }
    public string Street { get; set; }
    public string City { get; set; } public string ZipCode {get;set;}
}

 此时我们用BillingAddressId和FamlilyAddressId作为BillingAddress和FamliyAddress的导航属性。

此时我们用这两个来代表作为导航属性,但是Fluent API通过约定也并不认识,这是代表外键,因此我们需要手动添加外键:

        public UserMap()
        {
            HasRequired(a => a.BillingAddress)
                .WithMany().HasForeignKey(u => u.BillingAddressId);

            HasRequired(a => a.DeliveryAddress)
               .WithMany().HasForeignKey(u => u.FamlilyAddressId);
        }

 创建映射后添加数据进行尝试是否建立成功:

            EntityDbContext ctx = new EntityDbContext();
            var user = new User()
            {
                UserId = 1,
                BillingAddress = new Address() { AddressId = 1 },
                FamlilyAddress = new Address() { AddressId = 2 }
            };
            ctx.Set<User>().Add(user);
            ctx.SaveChanges();

一运行居然莫名其妙的出错了:

基于模型创建数据库过程中出现错误,有个多重级联路径。查阅相关资料得到如下结果:

因为在 SQL Server 表不能出现一次以上的所有级联参照动作由删除或更新语句启动列表中,您会收到此错误消息。例如,级联参照动作的树上级联引用操作树必须只能有一个到特定表的路径。

所以在User表上进行级联操作的删除或者更新的话肯定是不止一次,因为Address对应的两个Id即BillingAddressId和FamlilyAddressId,那进行映射时关掉一个级联操纵即可。于是乎最终改造如下:

        public UserMap()
        {
            HasRequired(a => a.BillingAddress)
                .WithMany().HasForeignKey(u => u.BillingAddressId);

            HasRequired(a => a.DeliveryAddress)
               .WithMany().HasForeignKey(u => u.DeliveryAddressId).WillCascadeOnDelete(false);
        }

 通过Sql  Profiler监控关键的添加外键约束语句如下:

ALTER TABLE [dbo].[Users] ADD CONSTRAINT [FK_dbo.Users_dbo.Addresses_BillingAddressId] FOREIGN KEY ([BillingAddressId]) REFERENCES [dbo].[Addresses] ([AddressId]) ON DELETE CASCADE

ALTER TABLE [dbo].[Users] ADD CONSTRAINT [FK_dbo.Users_dbo.Addresses_DeliveryAddressId] FOREIGN KEY ([DeliveryAddressId]) REFERENCES [dbo].[Addresses] ([AddressId])

 数据库关系图如下:

看上面Fluent API是不是有点惊讶,这和一对多的关系的配置是一样的。实际上我们把这看做是to-one即其实这种带外键的一对一关系非双向关系是一种单向关系,通过数据库关系图即可得知。那么我们如何使得它变成彻底的一对一的双向呢?我们上下文可以执行sql命名我们进行手动添加,我们现在试试:

我们在添加数据之前执行一段sql命令

  ctx.Database.ExecuteSqlCommand("ALTER TABLE Users ADD CONSTRAINT uc_Billing UNIQUE(BillingAddressId)");
  ctx.Database.ExecuteSqlCommand("ALTER TABLE Users ADD CONSTRAINT uc_Delivery UNIQUE(DeliveryAddressId)");

最终我们看重新生成的数据的关系图如下:

已经是完全的双向了,至此就完成了通过外键导航属性和唯一约束来实现一对一的关系

总结

有时候实现像上面的一对一实现不了就可以借用手写sql语句来实现,就像有时候用EF实现比较复杂的业务时也可以考虑用存储过程来实现。实现是多方式的,最主要还是的看怎样去实现最合适。当然以上所有列子在实际应用中不会这么奇葩,只是通过这样的例子来更加深入的学习一对一这样看似比较简单但实际上在三种关系中是比较麻烦的一种。

 

时间: 2024-10-09 12:04:11

EntityFramework之一对一关系(二)的相关文章

onetwoone-hibernate一对一关系的配置问题

问题描述 hibernate一对一关系的配置问题 一对一关系,怎么走一个添加方法,然后把数据添加到两张表中,修改的时候也是修改两张表中的数据,求解? 解决方案 Hibernate中关于一对一关系Hibernate关联关系配置(一对多.一对一和多对多)Hibernate关联关系配置(一对多.一对一和多对多) 解决方案二: 手动维护呗,控制在一个事务之内就行了. 解决方案三: 已我现在知道的,你可以在一个方法里面分别对这两个表对应的实体类对象进行操作.不过hibernate好像对于一对一关联的也有相

Hibernate的一对一关系

一对一关系的维护有两种,分为主键关联和唯一外键关联一对一关系的维护有两种,分为主键关联和唯一外键关联主键关联:(双向)(主控方) Person.javaPerson {    private String username;    //对被控方的引用    private Address address;    public String getUsername() {        return username;    }    public void setUsername(String 

ios-访问一对一关系引发了崩溃

问题描述 访问一对一关系引发了崩溃 有两个表Assets和Rewards,对应关系是一对一.我想访问reward表. NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Assets"]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"assetsId == %@",str]; [request setP

EntityFramework Core映射关系详解

前言 Hello,开始回归开始每周更新一到两篇博客,本节我们回归下EF Core基础,来讲述EF Core中到底是如何映射的,废话少说,我们开始. One-Many Relationship(一对多关系) 首先我们从最简单的一对多关系说起,我们给出需要映射的两个类,一个是Blog,另外一个则是Post,如下: public class Blog { public int Id { get; set; } public int Count { get; set; } public string N

Nhibernate一对一关系映射概述

现有两个表:user(用户)和Blog(设置表),它们之间的关系正如我所说的是一对一的关系.现在我们来映射这两个文件: <?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="MyMvc4Project.Models" assembly="MyMv

第九章 关系映射 一对一关系 唯一外键方式实现一对一

如:person与idCard 基于外键的one-to-one可以描述为多对一.   hibernate 一对一唯一外键关联映射(双向关联 Person<---->IdCard )           一对一唯一外键 双向 关联,需要在另一端(person ),添加 <one-to-one> 标签,指示 hibernate 如何加载 其关联对象,默认根据主键加载idcard ,外键关联映射中,因为两个实体采用的是 idcard 的外键维护的关系, 所以不能指定主键加载 idcard

Hibernate一对一关系

问题描述 我有一个user(用户)表,用户表里有个status(状态),一个user只能有一个status,由于便于管理我们把库里所有的状态都提了出来放在status_tbl表中,user表里的status对应着status_tbl中的status_ID,那么对user而言这种关系是什么关系呢?是多对一的关系还是一对一的关系?  我觉得是多对一的关系,为什么有人认为是一对一的关系,而且项目中一直用的是一对一的关系,不理解! 解决方案 一个User对应一个Status,一个Status可能对应多个

Nhibernate 一对一关系映射(主键映射)

参考:点击这里 妈的,搞了一天了,终于可以了,现在总结下,以防下次再出现这样痛苦的问题了,有两个表:user(用户)和Blog(设置表),它们之间的关系正如我所说的是一对一的关系.现在我们来映射这两个文件: <?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="My

精通Hibernate之映射继承关系二

把每个具体类映射到一张表是最简单的映射方式.如图14-2所示,在关系数据模型中只需定义COMPANIES.HOURLY_EMPLOYEES和SALARIED_EMPLOYEES表.为了叙述的方便,下文把HOURLY_EMPLOYEES表简称为HE表,把SALARIED_EMPLOYEES表简称为SE表. HourlyEmployee类和HE表对应,HourlyEmployee类本身的rate属性,以及从Employee类中继承的id属性和name属性,在HE表中都有对应的字段.此外,Hourly