如何在SQL Server中创建真正的一对一关系

Jam*_*mes 40 sql sql-server entity-framework one-to-one

我想在SQL Server 2008 R2中创建一对一的关系.

我有两个表tableAtableB,我设置tableB的主键的外键引用其中tableA的首要.但是当我首先使用Entity Framework数据库时,模型是1到0..1.

任何人都知道如何在数据库中创建真正的1对1关系?

提前致谢!

Pra*_*ana 60

将外键设置为主键,然后在两个主键字段上设置关系.而已!你应该在关系线的两端看到一个关键标志.这代表一对一.

在此输入图像描述

检查:具有一对一关系的SQL Server数据库设计

  • 这将在EF中产生1到0..1的关系. (21认同)
  • 实际上,没有必要在外键约束中使用主键列来实现1到0..1的关系.这里唯一重要的是对第二个表中用于外键的列的UNIQUE约束.顺便说一句,PK总是独一无二的. (5认同)
  • @ErikPhilips如何在不使用图表的情况下编写查询来实现这一目标? (4认同)

Eri*_*ips 54

我很确定在SQL Server中技术上不可能有一个真正的1对1关系,因为这意味着你必须同时插入两个记录(否则你会在插入时遇到约束错误),两个表,两个表彼此具有外键关系.

话虽这么说,用外键描述的数据库设计是1到0..1的关系.没有约束可能需要tableB中的记录.您可以与触发器建立伪关系,以在tableB中创建记录.

所以有一些伪解决方案

首先,将所有数据存储在一个表中.那么你在EF中没有任何问题.

或者其次,您的实体必须足够聪明,不允许插入,除非它有相关记录.

或者第三,也是最有可能的,你有一个问题,你试图解决,而你问我们为什么你的解决方案不起作用而不是你想要解决的实际问题(一个XY问题).

UPDATE

为了解释现实中 1对1关系如何不起作用,我将使用的类比或鸡蛋困境.我不打算解决这个难题,但是如果你有一个约束,为了在鸡蛋表中添加一个鸡蛋,必须存在鸡的关系,鸡必须存在于表中,然后你无法在Egg桌上添加一个Egg.反之亦然.如果没有与Egg表中存在的Egg和Egg的关系,则不能将Chicken添加到Chicken表中.因此,在不破坏其中一个规则/约束的情况下,不会在数据库中创建任何记录.

数据库一对一关系的命名是误导性的.我见过的所有关系(根据我的经验)将更具描述性,即一对一(零或一)关系.

  • @Igor:"在关系数据库理论中存在一对一的关系" - 如果你接受这种关系存在于现实中那么我们需要一种建模方法(如果我们的数据库系统不能处理它们然后我们应该要求新的系统!)Chris Date撰写了一篇关于关系性质主题的简短而全面的论文,并证明了10种关系类型的分类:[All For One,One For All](http:/ /www.dcs.warwick.ac.uk/~hugh/TTM/AllforOne.pdf). (5认同)
  • 我说过这样的1对1可以使用触发器(或任何proc,甚至可以禁用约束,然后启用它),但是如果你必须禁用约束来添加行,你就是在规避约束的整个意图首先.请继续评论数据库理论,我试图解释它是如何工作的,特别是在实体框架方面. (3认同)
  • @GlaucoCucchiar 这是一个名为 *1-to-1* 的术语,但是从技术上讲,它是 *1-to-0..1* 并且在 EF 中它 **将** 生成 *1-to-0..1*关系。 (2认同)

小智 24

这可以通过创建简单的主外键关系并以下列方式将外键列设置为唯一来完成:

CREATE TABLE [Employee] (
    [ID]    INT PRIMARY KEY
,   [Name]  VARCHAR(50)
);

CREATE TABLE [Salary] (
    [EmployeeID]    INT UNIQUE NOT NULL
,   [SalaryAmount]  INT 
);

ALTER TABLE [Salary]
ADD CONSTRAINT FK_Salary_Employee FOREIGN KEY([EmployeeID]) 
    REFERENCES [Employee]([ID]);
Run Code Online (Sandbox Code Playgroud)

架构

INSERT INTO [Employee] (
    [ID]
,   [Name]
)
VALUES
    (1, 'Ram')
,   (2, 'Rahim')
,   (3, 'Pankaj')
,   (4, 'Mohan');

INSERT INTO [Salary] (
    [EmployeeID]
,   [SalaryAmount]
)
VALUES
    (1, 2000)
,   (2, 3000)
,   (3, 2500)
,   (4, 3000);
Run Code Online (Sandbox Code Playgroud)

检查一切是否正常

SELECT * FROM [Employee];
SELECT * FROM [Salary];
Run Code Online (Sandbox Code Playgroud)

现在一般在主外交关系(一对多)中,您可以多次输入EmployeeID,但这里会抛出错误

INSERT INTO [Salary] (
    [EmployeeID]
,   [SalaryAmount]
)
VALUES
    (1, 3000);
Run Code Online (Sandbox Code Playgroud)

以上语句将显示错误

CREATE TABLE [Employee] (
    [ID]    INT PRIMARY KEY
,   [Name]  VARCHAR(50)
);

CREATE TABLE [Salary] (
    [EmployeeID]    INT UNIQUE NOT NULL
,   [SalaryAmount]  INT 
);

ALTER TABLE [Salary]
ADD CONSTRAINT FK_Salary_Employee FOREIGN KEY([EmployeeID]) 
    REFERENCES [Employee]([ID]);
Run Code Online (Sandbox Code Playgroud)

  • 这将在Entity Framework中产生1到0..1的关系.您创建了一个没有现有薪资的员工. (11认同)
  • 不管 EF - 可以创建一个没有薪水的 Employee 一整天。这是 1 到 0..1 (2认同)

tom*_*ius 5

我知道如何在不使用触发器、计算列、附加表或其他“奇异”技巧(仅外键和唯一约束)的情况下实现严格*一对一关系,但有一个小警告。

我将从接受的答案中借用鸡和蛋的概念来帮助我解释警告。

事实是必须先有鸡或有蛋(无论如何在当前的数据库中)。幸运的是,这个解决方案没有政治性,也没有规定哪个必须先出现——它留给了实施者。

需要注意的是,在技术上允许记录“先来”的表可以在没有其他表中相应记录的情况下创建记录;然而,在这个解决方案中,只允许一个这样的记录。当只创建一个记录(只有鸡或蛋)时,在删除“孤独”记录或在另一个表中创建匹配记录之前,不能向两个表中的任何一个添加更多记录。

解决方案:

向每个表添加外键,引用另一个,为每个外键添加唯一约束,并使一个外键可以为空,另一个不可为空,也是主键。为此,可空列上的唯一约束必须只允许一个空值(在 SQL Server 中是这种情况,其他数据库不确定)。

CREATE TABLE dbo.Egg (
    ID int identity(1,1) not null,
    Chicken int null,
    CONSTRAINT [PK_Egg] PRIMARY KEY CLUSTERED ([ID] ASC) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE dbo.Chicken (
    Egg int not null,
    CONSTRAINT [PK_Chicken] PRIMARY KEY CLUSTERED ([Egg] ASC) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE dbo.Egg  WITH NOCHECK ADD  CONSTRAINT [FK_Egg_Chicken] FOREIGN KEY([Chicken]) REFERENCES [dbo].[Chicken] ([Egg])
GO
ALTER TABLE dbo.Chicken  WITH NOCHECK ADD  CONSTRAINT [FK_Chicken_Egg] FOREIGN KEY([Egg]) REFERENCES [dbo].[Egg] ([ID])
GO
ALTER TABLE dbo.Egg WITH NOCHECK ADD CONSTRAINT [UQ_Egg_Chicken] UNIQUE([Chicken])
GO
ALTER TABLE dbo.Chicken WITH NOCHECK ADD CONSTRAINT [UQ_Chicken_Egg] UNIQUE([Egg])
GO
Run Code Online (Sandbox Code Playgroud)

要插入,首先必须插入一个鸡蛋(Chicken 为 null)。现在,只能插入一只鸡,并且它必须引用“无人认领”的鸡蛋。最后,添加的鸡蛋可以更新,它必须引用“无人认领”的鸡。任何时候都不能让两只鸡参考同一个鸡蛋,反之亦然。

要删除,可以遵循相同的逻辑:将鸡蛋的鸡更新为空,删除新“无人认领”的鸡,删除鸡蛋。

该解决方案还允许轻松交换。有趣的是,交换可能是使用这种解决方案的最有力的论据,因为它具有潜在的实际用途。通常情况下,在大多数情况下,通过简单地将两个表重构为一个来更好地实现两个表的一对一关系;然而,在一个潜在的场景中,这两个表可能代表真正不同的实体,这需要严格的一对一关系,但需要经常交换“伙伴”或重新安排,同时仍然保持一对一关系-重新安排后的关系。如果使用更常见的解决方案,则必须为重新排列的所有对更新/覆盖其中一个实体的所有数据列,与此解决方案相反,

嗯,这是我可以使用标准约束做的最好的事情(不要判断:)也许有人会发现它很有用。

  • 当然,在其他 SQL 数据库服务器(如 Oracle 或 PostgreSQL)中,这可以通过在提交事务时检查的可延迟约束正确完成,而不是在插入行时检查,从而避免鸡和蛋的问题。令人惊讶的是,MS 忽略了 SQL-92 标准的这部分,现在已经有 25 年的历史了。 (3认同)
  • 这将在实体框架中产生 1 到 0..1 的关系。 (2认同)
  • 在 MS SQL Server 中获得真正的 1:1 关系的另一种更长期的方法是投票支持这个连接项目:https://connect.microsoft.com/SQLServer/feedback/details/124728/option-to-defer-foreign -key-constraint-checking-until-transaction-commit (2认同)

Dai*_*Dai 5

如何在 SQL Server 中创建一对一关系?

简短的回答:你不能。

长答案:你可以,如果你敢继续读下去......


据我了解,当 DBMS 不支持可延迟约束时,有两种主要方法可以“实现”1:1 关系(*咳嗽* MS SQL Server *咳嗽*)。这篇文章讨论了这两种主要方法。

这两种方法都通过欺骗 EF 将 a 视为 来与 EF 具有一定程度的VIEW兼容性TABLE。如果您不使用 EF,那么您可能不需要这些VIEW对象,但它们仍然可以方便地进行方便查询并快速查询单独表中实体的产品类型1:1视图。

这两种方法都是围绕使用另一个表 ( ValidCountries) 构建的,该表包含PK 值,其存在有两个原因:

  1. 对两个1:1成员表都有 FK 约束(不要忘记您也可以拥有三个或更多1:1表!):因此,除非所有必需的相关数据都存在于各自的表中,否则ValidCountries 不能存在行。
  2. 为来自其他FOREIGN KEY实体的任何传入约束提供目标。下面对此进行更详细的解释和演示。

这两种方法的不同之处在于对1:1成员表的约束、对象的使用TRIGGER以及与 EF 的兼容性。我确信这两种方法可能有更多变化 - 这实际上取决于您如何建模数据和业务需求。

这两种方法都不使用CHECK CONSTRAINTUDF 规则来验证其他表中的数据,这是目前实现1:1约束的主要方法,但该方法的性能声誉不佳。


方法 1:使用另外两个 TABLE对象(一个用于前向声明,另一个作为有效性证明)和一个读/写对象VIEW来仅公开1:1来自 a 的有效数据JOIN

  • 此方法使用第三个表“前向声明” (共享)PK 值,而希望1:1彼此建立关系的其他表引用前向声明表。

  • 另一个“最终”TABLE用于证明(通过 FK 约束)对于任何给定的 PK,有效的肯定存在。

  • 然后,这种复杂性隐藏在一个(技术上可选的)VIEW对象后面,该对象仅公开有效数据并执行INNER JOIN3 个(或更多)支持表中的一个,同时还支持INSERT/UPDATE/DELETE/MERGEDML 操作。

    • 这对于实体框架非常有效,因为 EF 非常乐意假装 aVIEWTABLE. 需要注意的是,所有这些方法都是严格数据库优先的,因为所有这些方法都比 EF更聪明,可以使其屈服于我们的意愿(因此请务必禁用迁移!)
    • 虽然“最终”表可能看起来多余,因为它VIEW永远不会暴露无效数据,但实际上非常有必要作为来自其他单独实体表的传入外键引用的目标(绝不能引用前向声明表) 。
  • 这三个表是:

    1. 表1 :包含PK值的“前向声明表” 。
    • 在 OP 的示例中(ofCountriesCapitals),这将是一个名为 like CountryDeclarations(或CountryDecl简称)的表,并且仅存 储值,这是和表CountryName的共享 PK )。CountriesCapitals
    1. 表 2:一个(或多个!)具有前向声明表 FK 的从属表。
      • 在OP的示例中,这将是两个表:
        • TABLE CountrieswithCountryName作为表的 PK及其FK 仅用于前向声明表。
        • TABLE CapitalswithCountryName作为表的 PK及其FK 仅用于前向声明表。
    2. 表 3:公开可见的主体表,其中具有前向声明表和所有依赖表的 FK。
    • 在OP的示例中,这将使用TABLE ValidCountriesPK + FK toCountryDecl和分隔FK列 toCountriesCapitals

这是此方法的数据库图:

在此输入图像描述

  • 当从Countries和/或Capitals表中查询数据时,只要始终提供INNER JOINValidCountries那么您就可以得到始终查询有效数据的硬保证

    • 或者只是使用 来为您完成已经完成的VIEW工作。JOIN
  • 请记住,成分和表之间的1:1关系不是强制的:这是必要的,否则会出现先有鸡还是先有蛋的问题。CountriesCapitalsINSERT

    • 虽然如果您确定总是会INSERT进入Countriesbefore Capitals(并且DELETE以相反的顺序),您可以直接添加一个FK约束 from到,但这并没有真正增加任何好处,因为表无法保证相应的行将存在。CapitalsCountriesCountriesCapitals
  • IDENTITY这种设计也与 PK兼容,只需记住,只有前向声明表才会有该IDENTITY列,所有其他表都会有正常的intPK+FK 列。

下面是该方法的 SQL:

CREATE SCHEMA app1; /* The `app1` schema contains the individual objects to avoid namespace pollution in `dbo`. */
GO

CREATE TABLE app1.CountryDecl (
    CountryName nvarchar(100) NOT NULL,
    CONSTRAINT PK_CountryDecl PRIMARY KEY ( CountryName )
);
GO

CREATE TABLE app1.Countries (
    CountryName nvarchar(100) NOT NULL,
    CapitalName nvarchar(255) NOT NULL,
    Inhabitants bigint        NOT NULL,
    AreaKM2     bigint        NOT NULL,

    CONSTRAINT PK_Countries PRIMARY KEY ( CountryName ),

    CONSTRAINT FK_CountriesDecl FOREIGN KEY ( CountryName ) REFERENCES app1.CountryDecl ( CountryName ),
--  CONSTRAINT FK_Countries_Capitals FOREIGN KEY ( CountryName ) REFERENCES app1.Capitals ( CountryName ) -- This FK is entirely optional and adds no value, imo.
);
GO

CREATE TABLE app1.Capitals (
    CountryName nvarchar(100) NOT NULL,
    CapitalName nvarchar(255) NOT NULL,
    Inhabitants bigint        NOT NULL,
    AreaKM2     int           NOT NULL,

    CONSTRAINT PK_Capitals PRIMARY KEY ( CountryName ),

    CONSTRAINT FK_CountriesDecl FOREIGN KEY ( CountryName ) REFERENCES app1.CountryDecl ( CountryName )
);
GO

CREATE TABLE app1.ValidCountries (

    CountryName nvarchar(100) NOT NULL,

    CONSTRAINT PK_ValidCountries PRIMARY KEY ( CountryName ),

    CONSTRAINT FK_ValidCountries_to_Capitals FOREIGN KEY ( CountryName ) REFERENCES app1.Capitals ( CountryName ),
    CONSTRAINT FK_ValidCountries_to_Countries FOREIGN KEY ( CountryName ) REFERENCES app1.Countries ( CountryName ).
    CONSTRAINT FK_ValidCountries_to_Decl FOREIGN KEY( CountryName ) REFERENCES app1.CountriesDecl ( CountryName )
);
GO

CREATE VIEW dbo.Countries AS
SELECT
    -- ValidCountries:
    v.CountryName,

    -- Countries
    cun.Inhabitants     AS CountryInhabitants,
    cun.Area            AS CountryArea,

    -- Capitals
    cap.Capital         AS CapitalCityName,
    cap.CityArea        AS CapitalCityArea,
    cap.CityInhabitants AS CapitalCityInhabitants

FROM
    app1.ValidCountries AS v
    INNER JOIN app1.Countries AS cun ON v.CountryName = cun.CountryName
    INNER JOIN app1.Capitals  AS cap ON v.CountryName = cap.CountryName;

GO

CREATE TRIGGER Countries_Insert ON dbo.Countries
INSTEAD OF INSERT 
AS
BEGIN

    SET NOCOUNT ON;
    
    INSERT INTO app1.CountriesDecl (
        CountryName
    )
    SELECT
        CountryName
    FROM
        inserted;

    -------

    INSERT INTO app1.Capitals (
        CountryName,
        Capital,
        CityInhabitants,
        CityArea
    )
    SELECT
        CountryName,
        CapitalCityName,
        CapitalCityInhabitants,
        CapitalCityArea
    FROM
        inserted;

    -------

    INSERT INTO app1.Countries (
        CountryName,
        Capital,
        Inhabitants,
        Area
    )
    SELECT
        CountryName,
        CapitalCityName,
        CountryInhabitants,
        CountryArea
    FROM
        inserted;

    ----

    INSERT INTO app1.ValidCountries (
        CountryName
    )
    SELECT
        CountryName
    FROM
        inserted;

    -------

END;

/* NOTE: Defining UPDATE and DELETE triggers for the VIEW is an exercise for the reader. */
Run Code Online (Sandbox Code Playgroud)
  • 使用 Entity Framework 和 Entity Framework Core 时,请记住,此类方法最终是为了智胜Entity Framework(如果不是彻底的黑客攻击),因此永远不要让 EF 执行任何迁移或生成和运行任何 DDL(CREATE TABLE.. .) 基于您的 Code-First 实体模型类的语句。
    • 虽然 EF 不再支持“数据库优先”模型,但您仍然可以将“代码优先从数据库”与代码优先代码生成一起使用,例如https://github.com/sjh37/EntityFramework-Reverse-POCO-Code- First-Generator(免责声明:这是我个人最喜欢的代码生成器,我是该项目的贡献者)。

    • 如果您使用这种方法在数据库上运行默认脚手架或 code-first-codegen ,那么您最终会得到一个包含app1.Countriesapp1.Capitalsapp1.CountriesDecl的单独实体的模型app1.ValidCountries- 因此您应该配置您的 code-gen 以过滤掉那些您不需要的对象不希望出现在您的 EF 模型中。

      • 在这种情况下,我会从 EF 中排除所有app1.*表,而是指示 EF 将其视为VIEW dbo.Countries单个实体(这是有道理的,因为从数学上讲,两个实体之间的每个 关系都与定义为这些实体的产品类型的单个实体1:1相同)2 个其他实体)。
      • 因为 aVIEW没有 aPRIMARY KEY也没有任何FOREIGN KEY约束,所以 EF(默认情况下)无法从 a 正确编码生成实体类VIEW,但是前面提到的代码生成工具可以轻松地以正确的方式推动 EF(查找方法ViewProcessing,以及AddForeignKeys下面的方法)它)。
    • 如果您确实app1.Countries将和表保留app1.Capitals为 EF 中的实体类型,请注意,让 EFINSERT对这两个表执行 into 将会失败,除非您的代码首先执行INSERTinto app1.CountriesDecl

    • 或者您可以添加一个CREATE TRIGGER Countries/Capitals_Insert ON app1.Countries/app1.Capitals INSTEAD OF INSERT将执行IF NOT EXIST ... INSERT INTO app1.CountriesDecl.

    • UPDATE然而,至少 EF 在这两个表上不会有任何问题DELETE


方法 2:只有一个额外的TABLE对象,但FK列是NULL可以的 - 并且 aVIEW用作窗帘来隐藏无效/不完整的行。

  • 如果方法 1可以概括为借用“对象必须始终是不可变的”思想流派的思想,那么方法 2的灵感来自于允许您就地改变现有对象的语言,以便编译器可以验证每个对象突变步骤改变对象的有效类型,使其满足某些类型约束。

    • 例如,考虑这个TypeScript(因为截至 2022 年,TypeScript 似乎仍然不支持/检测向 POJsO 添加属性(从而扩展其结构类型)是否有效并可证明扩展变量的静态类型):

      interface MyResult { readonly name: string; readonly year: number; };
      
      function doSomething() : MyResult {
          let result = {};
      //  return result;                 // Error: Cannot return `result` yet: it doesn't conform to `MyResult` (there's no `name` nor `year` value)
          result.name = "NameGoesHere";  // So let's define `name`.
      //  return result;                 // ERROR: Still cannot return `result` yet: it still doesn't yet have a `year` property.
          result.year = 2022;            // So let's add `year`.
          return result;                 // No error, `result` can now be returned OK because it conforms to `interface MyResult`.
      }
      
      Run Code Online (Sandbox Code Playgroud)
  • 考虑到这个概念,我们可以拥有TABLE保存部分/不完整的对象CountryCapital可以自由插入/更新/删除的数据,因为它们的相互FOREIGN KEY约束是NULL可以的,见下文。

    • 这些表分别被命名为dbo.CountriesData和 ,dbo.CapitalsData而不是dbo.Countriesdbo.Capitals,以表明这些表仅包含任意“数据”而不是有效且正确的实体。这是我个人的命名约定。YMMV。
    • 方法 1一样,存在仅将有效VIEW dbo.Countries实体公开为单一产品类型
      • 或者,您还可以VIEW分别为国家和首都定义其他对象,并让 EF 将它们也视为实体(尽管您需要加载更多的跑腿工作来INSERT单独为每个视图进行工作)。
  • 但与方法 1不同的是,该dbo.CapitalsData表现在具有复合主键,这是 OP 的特定数据库设计目标的结果 - 这可能不适用于您的数据库。

    • 复合 PK 是必要的,以允许在强制执行约束的情况下dbo.Countries具有非值。这是必要的,因为也是PK的,所以不可能。这是可行的,因为 SQL Server 仅当FK 中的所有列都是非列时才强制执行 FK 约束。如果你有不同的PK设计,那么这对你来说将会有所不同。NULL CountryNameFK_CountriesData_to_CapitalsCountryNamedbo.CountriesDataNULLNULL
CREATE TABLE dbo.CountriesData (
   CountryName nvarchar(100) NOT NULL,
   CapitalName nvarchar(255)     NULL,
   Inhabitants bigint        NOT NULL,
   Area        geography     NOT NULL,

   CONSTRAINT PK_CountriesData PRIMARY KEY ( CountryName ),

   CONSTRAINT FK_CountriesData_to_Capitals FOREIGN KEY ( CountryName, CapitalName ) REFERENCES dbo.CapitalsData ( CapitalName )
);

CREATE TABLE dbo.CapitalsData (
    CountryName nvarchar(100) NOT NULL,
    CapitalName nvarchar(255) NOT NULL,
    Inhabitants bigint        NOT NULL,
    Area        geography     NOT NULL,

    CONSTRAINT PK_CapitalsData PRIMARY KEY ( CountryName, CountryName ),

    CONSTRAINT FK_CapitalssData_to_Countries FOREIGN KEY ( CapitalName ) REFERENCES dbo.CountriesData ( CountryName )
);

CREATE VIEW dbo.Countries AS
SELECT
    -- Countries
    cun.Inhabitants     AS CountryInhabitants,
    cun.Area            AS CountryArea,

    -- Capitals
    cap.Capital         AS CapitalCityName,
    cap.CityArea        AS CapitalCityArea,
    cap.CityInhabitants AS CapitalCityInhabitants

FROM
    dbo.CountriesData AS cd
    INNER JOIN dbo.CapitalsData AS cad ON cd.CountryName = cad.CountryName;


CREATE TABLE dbo.ValidCountries (

   -- This TABLE is largely the as in Approach 1. Ensure that all incoming FKs only reference this table and not dbo.CountriesData or dbo.CapitalsData.
   -- NOTE: When using EF, provided to trick EF into treating `VIEW dbo.Countries` as a TABLE then you don't need to include this table in your EF model at all (just be sure to massage all of EF's FK relationships from other entities that initially point to `ValidCountries` to point to the `VIEW dbo.Countries` entity instead.

    CountryName nvarchar(100) NOT NULL,
    CapitalName nvarchar(255) NOT NULL,

    CONSTRAINT PK_ValidCountries PRIMARY KEY ( CountryName ),

    CONSTRAINT FK_ValidCountries_to_Capitals FOREIGN KEY ( CountryName ) REFERENCES dbo.CapitalsData ( CountryName, CapitalName ),
    CONSTRAINT FK_ValidCountries_to_Countries FOREIGN KEY ( CountryName ) REFERENCES dbo.CountriesData ( CountryName )
);

CREATE TRIGGER After_UPDATE_in_CountriesData_then_INSERT_into_ValidCountries_if_valid ON dbo.CountriesData
AFTER UPDATE 
AS
BEGIN
    INSERT INTO dbo.ValidCountries ( CountryName, CapitalName )
    SELECT
        i.CountryName,
        i.CapitalName
    FROM
        inserted.CountryName AS i
        INNER JOIN dbo.CapitalsData AS capd ON -- The JOINs prevents inserting CountryNames for countries that are either invalid or already exist in dbo.ValidCountries.
            capd.CountryName = i.CountryName
            AND
            capd.CapitalName = i.CapitalName
        LEFT OUTER JOIN dbo.ValidCountries AS v ON -- This is a "LEFT ANTI JOIN" due to the WHERE condition below.
            v.CountryName = i.CountryName
    WHERE 
        v.CountryName IS NULL
        AND
        i.CapitalName IS NOT NULL;
END;

CREATE TRIGGER After_INSERT_in_CapitalsData_then_SET_C ON dbo.CapitalsData
AFTER INSERT 
AS
BEGIN
    
    -- Due to the specific design of dbo.CapitalsData, any INSERT will necessarily complete a valid product-type entity, so we can UPDATE dbo.CountriesData to set CapitalName to the correct value.
    UPDATE
        cd
    SET
        cd.CapitalName = inserted.CapitalName
    FROM
        dbo.CountriesData AS cd
        INNER JOIN inserted AS i ON
            cd.CountryName = i.CountryName
            AND
            cd.CapitalName IS NULL
    WHERE
        i.CountryName IS NOT NULL;
 
END;
Run Code Online (Sandbox Code Playgroud)
  • 对于手动 DML:
    • INSERT一个新的国家...
      1. 首先INSERT INTO dbo.CountriesData有一个初始NULL CapitalName值。
      • 这是可以的,因为当 FK 的值(或复合 FK 中至少有 1 个值)为 时,SQL Server 会忽略 FK 约束NULL
      1. 然后INSERT INTO dbo.CapitalsData(或反之亦然,前提CountryName是相反NULL)。
      2. 只有在插入两行之后,您才可以运行UPDATE dbo.CountriesData SET CapitalName = inserted.CapitalName WHERE CountryName = inserted.CountryName.
      3. VIEW dbo.Countries现在将公开现在有效的1:1相关数据。
    • DELETE操作必须以相反的顺序执行(即首先UPDATE清除 FK,然后DELETE以任意顺序清除每个表)。
    • UPDATE操作不需要特殊处理。
  • 我注意到,您实际上可以将上述所有INSERT逻辑移至和表AFTER INSERT上的触发器中,因为这意味着: CountriesDataCapitalsData
    • UPDATEAFTER INSERT触发了dbo.CapitalsData!(反之亦然) - 但一定要添加检查WHERE inserted.CountryName IS NOT NULL- 但如果你这样做,那么你的客户端的 SQL 代码只需要执行两个INSERT语句,两个触发器之一AFTER INSERT将自动处理其余部分,但前提是数据最终有效 - 因此它将在 中可见VIEW dbo.Countries
    • 这种方法在 EF 中效果更好,因为您不需要在表上闲逛CountriesDecl,因此对表进行单独的操作INSERT不会失败 - 但请记住,这两个表/实体之间没有关系。dbo.CountriesDatadbo.CapitalsData1:1