【问题标题】:How to Create a real one-to-one relationship in SQL Server如何在 SQL Server 中创建真正的一对一关系
【发布时间】:2012-05-04 18:16:58
【问题描述】:

我有两个表CountryCapital,我将Capital 的主键设置为引用Country 的主键的外键。但是当我首先使用实体​​框架数据库时,模型是 1 到 0..1。

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

【问题讨论】:

  • 在实践中。谁需要“真正的”一对一关系?类型 1 到 0..1 的关系是好的,更自然和实用。这就像在现有表中添加一个可为空的列。只需使外键唯一。
  • 问问自己«我想先向哪个表添加记录?»然后将 FK 添加到另一个。

标签: sql sql-server entity-framework one-to-one


【解决方案1】:

我很确定在 SQL Server 中建立真正的 1 对 1 关系在技术上是不可能的,因为这意味着您将必须同时插入两条记录(否则您d 在插入时出现约束错误),在两个表中,两个表之间具有外键关系。

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

所以有一些伪解决方案

首先,将所有数据存储在一个表中。那么你在 EF 中就没有问题了。

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

或者第三个,最有可能的是you have a problem you are trying to solve, and you are asking us why your solution doesn't work instead of the actual problem you are trying to solve (an XY Problem)

更新

为了在 REALITY 中解释一对一关系如何不起作用,我将使用Chicken or the egg dilemma 的类比。我不打算解决这个困境,但是如果你有一个约束说为了向 Egg 表中添加一个 Egg,Chicken 的关系必须存在,并且 Chicken 必须存在于表中,那么你不能在 Egg 表中添加一个 Egg。反之亦然。如果没有与 Egg 的关系以及 Egg 表中存在的 Egg,则无法将 Chicken 添加到 Chicken 表。因此,在不违反规则/约束之一的情况下,无法在数据库中创建每条记录。

一对一关系的

数据库命名法具有误导性。我见过的所有关系(因此是我的经验)将更具描述性,因为一对(零或一)关系。

【讨论】:

  • "...因为这意味着您必须同时插入两条记录" 所以一对多关系意味着您需要同时插入所有记录?
  • 不,因为一对多实际上是 1 对 0..(x)。仍然没有要求在多关系中有任何记录。 :D(这是专门关于 EF 如何描述关系的,而不是我)
  • @Igor:“关系数据库理论中存在一对一关系”——如果您接受这种关系在现实中存在,那么我们需要一种建模它们的方法(如果我们的数据库系统不能很好地处理它们,那么我们应该要求新的系统!)Chris Date 写了一篇关于关系性质主题的简短而详尽的论文,并证明了 10 种关系类型的分类:All For One, One For All
  • 我说过这样的 1 对 1 可以使用触发器(或任何 proc,甚至可以禁用约束,然后启用它)存在,但是如果您必须禁用约束才能添加一行,您首先是规避约束的全部意图。请停止评论数据库理论,我试图解释它实际上是如何工作的,尤其是在实体框架方面。
  • @GlaucoCucchiar 这是一个称为 1-to-1 的术语,但在技术上它是 1-to-0..1 AND 在 EF它生成1对0..1关系。
【解决方案2】:

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

检查这个:SQL Server Database Design with a One To One Relationship

【讨论】:

  • 这将在 EF 中产生 1 到 0..1 的关系。
  • @ErikPhilips 我们如何在不使用图表的情况下编写查询来实现这一点?
  • @jason 写一个查询是什么意思?你的意思是说,我如何用 T-SQL 中描述的关系 Pranay 创建两个表?
  • @ErikPhilips 是的,我是这个意思。
  • 其实并没有真正需要在外键约束中使用主键列来实现1对0..1的关系。这里唯一重要的是第二个表中用于外键的列的唯一约束。顺便说一句,PK 总是独一无二的。
【解决方案3】:

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

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]);

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);

检查是否一切正常

SELECT * FROM [Employee];
SELECT * FROM [Salary];

现在一般处于主要对外关系(一对多), 你可以多次输入EmployeeID, 但是这里会报错

INSERT INTO [Salary] (
    [EmployeeID]
,   [SalaryAmount]
)
VALUES
    (1, 3000);

上面的语句会显示错误

违反 UNIQUE KEY 约束“UQ__Salary__7AD04FF0C044141D”。 无法在对象“dbo.Salary”中插入重复键。重复键值为 (1)。

【讨论】:

  • 这将在实体框架中产生 1 到 0..1 的关系。您创建了一个没有现有薪水的员工。
  • 我对最后一个例子有点好奇。如果只是将 Salary 对象添加到 Employee 类中,我是否可以在 EF 中无需进一步操作,只需在代码中说: var Salary = Employee(Id).Salary???
  • 不管 EF - 可以整天创建一个没有薪水的员工。这是 1 到 0..1
  • 这被认为是最好的答案!我已经完全实现了@ahim 所说的,我在我的 SQL Server MS 上创建了一对一的关系。非常感谢!!!!
  • @PatsonLeaner 嗯,不,它是 1 到 0/1,如上所述。 Erik 的回答提供了所有信息。 SO上有很多1到0/1的答案。
【解决方案4】:

有一种方法我知道如何在不使用触发器、计算列、附加表或其他“奇异”技巧(仅外键和唯一约束)的情况下实现严格的*一对一关系,但有一点需要注意.

我将从公认的答案中借用先有鸡的概念来帮助我解释警告。

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

需要注意的是,从技术上讲,允许记录“先出现”的表可以创建一个记录,而另一个表中没有相应的记录;然而,在这个解决方案中,只允许一个这样的记录。如果只创建了一条记录(只有 chicken 或 egg),则在删除 'lonely' 记录或在另一个表中创建匹配记录之前,不能将更多记录添加到两个表中的任何一个。

解决方案:

为每个表添加外键,引用另一个,为每个外键添加唯一约束,并使一个外键可以为空,另一个不能为空,同时也是主键。为此,可空列上的唯一约束必须只允许一个空值(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

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

要删除,可以遵循相同的逻辑:将egg的Chicken更新为null,删除新的'unclaimed'鸡,删除egg。

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

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

【讨论】:

  • 这将在实体框架中产生 1 到 0..1 的关系。
  • 当然,在 Oracle 或 PostgreSQL 等其他 SQL 数据库服务器中,这可以通过在提交事务时而不是在插入行时检查可延迟约束来正确完成,从而避免鸡和- 鸡蛋问题。令人惊讶的是,MS 忽略了 SQL-92 标准的这一部分,该标准已经有 25 年历史了。
  • 在 MS SQL Server 中获得真正的 1:1 关系的另一种更长期的方法是投票支持此连接项:connect.microsoft.com/SQLServer/feedback/details/124728/…
  • 无论如何-谢谢您的回答-这似乎是我数据库的最佳解决方法。赞赏
【解决方案5】:

SQL中的1对1关系是通过将两个表的字段合并为一个来建立的!

我知道您可以将表拆分为具有 1 对 1 关系的两个实体。大多数时候你使用它是因为你想在“表中二进制数据的重字段”上使用延迟加载。

示例:您有一个包含图片的表格,其中包含名称列(字符串)、可能是一些元数据列、缩略图列和图片本身 varbinary(max)。在您的应用程序中,您肯定会首先在集合控件中仅显示名称和缩略图,然后仅在需要时才加载“完整图片数据”。

如果它是您正在寻找的。这就是所谓的“表拆分”或“水平拆分”。

https://visualstudiomagazine.com/articles/2014/09/01/splitting-tables.aspx

【讨论】:

  • 这将在实体框架中产生 1 到 0..1 的关系。
【解决方案6】:

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

我了解当 DBMS 支持可延迟约束时,有两种主要方法可以“实现”1:1 关系。这两种方法都需要两个主表中 other-half 表的外键。

  1. 使用第三个表来前向声明 PK 值。
    • 三个表:
      1. 只有 PK 值的转发声明表。
      2. 一个或多个具有 FK 到前向声明表的依赖表。
      3. 最终的主表,它对前向声明表和所有依赖表都有 FK。
    • 这是最简单的实现方式,可能具有最佳的整体性能,但会使INSERT 操作稍微复杂一些。
    • 缺点是second表中存在行并不意味着主表行存在。
    • 这与IDENTITY 兼容,但IDENTITY 必须在前向声明表中,而不是在主表中。
  2. 允许半表一个中的外键列是NULL,但只允许通过VIEW 访问数据,该VIEW 只公开具有有效(非@ 987654326@)。
    • 您可以将INSERT 直接支持到VIEW 中(这意味着它可以与大多数ORM 配合使用,包括实体框架)。
    • 您还可以通过将其设为INDEXED VIEW 来提高性能。

选项 1:使用第三个表进行前向声明:

通过将系统视为不可变数据场景,我指出了与构建对象图的相似之处(并且在图中的节点之间具有任意关系,甚至是 1:1)。

通过添加(可选隐藏)第三个表和提供值的“前向声明”的视图,然后 可以被相关表引用,而“真正的”主表对(可选隐藏的)列有约束。

使用 OP 的示例,这是一个 1:1

CREATE TABLE dbo.CountryDecl (
    CountryName nvarchar(255) NOT NULL,
    CONSTRAINT PK_CountryDecl PRIMARY KEY ( CountryName )
);

CREATE TABLE dbo.Countries (
    CountryName nvarchar(255) 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 dbo.CountryDecl ( CountryName ),
    CONSTRAINT FK_Countries_Capitals FOREIGN KEY ( CountryName ) REFERENCES dbo.Capitals ( CountryName )
);

CREATE TABLE dbo.Capitals (
    CountryName nvarchar(255) 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 dbo.CountryDecl ( CountryName )
)

INSERT 操作如下所示:

INSERT INTO dbo.CountryDecl ( CountryName ) VALUES ( 'Kazakhstan' );

INSERT INTO dbo.Capitals ( CountryName, CapitalName, Inhabitants, Area ) VALUES ( 'Kazakhstan', 'Nur-Sultan', 1136008, 810 );

INSERT INTO dbo.Countries ( CountryName, CapitalName, Inhabitants, Area ) VALUES ( 'Kazakhstan', 'Nur-Sultan', 18711560, 2724900 );

保证dbo.Countries 的所有查询总是返回有效行(即,如果dbo.Countries 中有一行,那么dbo.Capitals 中也总是有一个有效行),但这不是传递性的:一行在dbo.Capitals 中不保证dbo.Countries 中存在一行。


选项 2a:使用VIEW 作为隐藏无效行的幕布(使用NULLable FK):

CREATE TABLE dbo.CountriesData (
    CountryName nvarchar(255) NOT NULL,
    CapitalName nvarchar(255)     NULL,
    Inhabitants bigint        NOT NULL,
    AreaKM2     bigint        NOT NULL,

    CONSTRAINT PK_Countries PRIMARY KEY ( CountryName ),

    CONSTRAINT FK_Countries_Capitals FOREIGN KEY ( CountryName ) REFERENCES dbo.Capitals ( CountryName )
);

CREATE TABLE dbo.Capitals (
    CountryName nvarchar(255) NOT NULL,
    CapitalName nvarchar(255) NOT NULL,
    Inhabitants bigint            NULL, /* Inhabitants and AreaKM2 need to be NULL as INSERTing into dbo.CountriesData means a dbo.Capitals row will be created, but we don't have any values for these columns at-that-point, but if you have that data then these columns *can* be NOT NULL. So this is not a limitation of this approach, only your domain-model. */
    AreaKM2     int               NULL,

    CONSTRAINT PK_Capitals PRIMARY KEY ( CountryName ),

    CONSTRAINT FK_CountriesDecl FOREIGN KEY ( CountryName ) REFERENCES dbo.CountryDecl ( CountryName )
);

CREATE VIEW dbo.Countries AS
    SELECT
        CountryName,
        CapitalName,
        Inhabitants,
        AreaKM2
    FROM
        dbo.CountriesData
    WHERE
        CapitalName IS NOT NULL;

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

    SET NOCOUNT ON;

    INSERT INTO dbo.CountriesData ( CountryName, CapitalName, Inhabitants, AreaKM2 )
    SELECT CountryName, NULL, Inhabitants, AreaKM2 FROM [inserted];

    INSERT INTO dbo.Capitals ( CountryName, CapitalName, Inhabitants, AreaKM2 )
    SELECT CountryName, CapitalName, NULL, NULL FROM [inserted];

    UPDATE
        cd
    SET
        cd.CapitalName = ic.CapitalName
    FROM
        dbo.CountriesData AS cd
        INNER JOIN [inserted] AS ic ON ic.CountryName = cd.CountryName
    WHERE
        cd.CapitalName IS NULL;

END

选项 2b:使用VIEW 作为隐藏无效行的幕布(没有NULLable 列,但在VIEW 中使用JOIN):

CREATE TABLE dbo.CountriesData (
    CountryName nvarchar(255) NOT NULL, /* NOTE: there is no CapitalName column */
    Inhabitants bigint        NOT NULL,
    AreaKM2     bigint        NOT NULL,

    CONSTRAINT PK_Countries PRIMARY KEY ( CountryName ),

    CONSTRAINT FK_Countries_Capitals FOREIGN KEY ( CountryName ) REFERENCES dbo.Capitals ( CountryName )
);

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

    CONSTRAINT PK_Capitals PRIMARY KEY ( CountryName ),

    CONSTRAINT FK_CountriesDecl FOREIGN KEY ( CountryName ) REFERENCES dbo.CountryDecl ( CountryName )
);

CREATE VIEW dbo.Countries AS
    SELECT
        d.CountryName,
        c.CapitalName,
        d.Inhabitants,
        d.AreaKM2
    FROM
        dbo.CountriesData AS d
        INNER JOIN dbo.Capitals AS c ON d.CountryName = c.CountryName;

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

    SET NOCOUNT ON;

    INSERT INTO dbo.CountriesData ( CountryName, Inhabitants, AreaKM2 )
    SELECT CountryName, Inhabitants, AreaKM2 FROM [inserted];

    INSERT INTO dbo.Capitals ( CountryName, CapitalName, Inhabitants, AreaKM2 )
    SELECT CountryName, CapitalName, NULL, NULL FROM [inserted];

END

【讨论】:

  • 选项1;这将在实体框架中产生 1 到 0..1 的关系。选项 2,取决于模型构建器和实体的配置方式,可能会产生 1 到 0..1 或没有关系。 EF 仅支持 1 到 0..1 或 1 到 0..∞ 关系。对于 SQL Server,这是一个新颖的解决方案,我只提到 EF 中的结果,因为 OP 特别提到了它。
【解决方案7】:

实现此目的的最简单方法是仅创建 1 个表,其中表 A 和 B 字段都不是 NULL。这样就不可能有一个没有另一个。

【讨论】:

  • 这将在实体框架中产生 1 到 0..1 的关系。
  • @ErikPhilips,没关系。谁告诉你 EF 是 100% 正确的?
  • @ShadiNamrouti 我什么时候说 EF 是正确的?我只说明了结果,而不是对结果的看法。
【解决方案8】:

这个呢?

create table dbo.[Address]
(
Id int identity not null,
City nvarchar(255) not null,
Street nvarchar(255) not null,
CONSTRAINT PK_Address PRIMARY KEY (Id)
)

create table dbo.[Person]
(
Id int identity not null,
AddressId int not null,
FirstName nvarchar(255) not null,
LastName nvarchar(255) not null,
CONSTRAINT PK_Person PRIMARY KEY (Id),
CONSTRAINT FK_Person_Address FOREIGN KEY (AddressId) REFERENCES dbo.[Address] (Id)
)

【讨论】:

  • 这是提出问题还是提出答案?这显然是在其他答案中描述为 1:0 或 1 的情况。实际上它与问题中描述的代码相同。为什么这还不明显?您可以有一个没有人员行的地址行。
  • @philipxy 你是对的,我没有意识到创建额外的地址是一个问题。
  • 在这段代码中可能有没有人拥有的地址。您正在描述 1:0 或 1。我不知道你想用“Perspective”开头的“没关系”句子说什么。问题中的 1:1 意味着像 SQL PK FK 一样相互约束。再一次:阅读问题、答案和 cmets。等等。
  • 如果一个地址只有一个人或没有人,而一个人只有一个地址,那么 address:person 是 1:(0-or-1) 关系。再见。
  • 基数的方法可以分为两种方法:描述 n 元关系/表与描述其中一种加上从中提取值的单列表。 (图线标签也有 2 个阵营,看这里和看远。)这类似于函数基数的 2 种方法——描述一组键值对与描述其中一个加组,其中键和值被绘制 & 一个函数可以是全部或部分的。 (术语“域”、“范围”和“共同域”甚至没有标准含义,每个演示文稿都必须阐明其用法。)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-12
  • 1970-01-01
  • 2014-11-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多