我有两个表Country
,Capital
我将Capital
的主键设置为引用主键的外键Country
。但是当我首先使用实体框架数据库时,模型是 1 到 0..1。
如何在 SQL Server 中创建一对一关系?
我有两个表Country
,Capital
我将Capital
的主键设置为引用主键的外键Country
。但是当我首先使用实体框架数据库时,模型是 1 到 0..1。
如何在 SQL Server 中创建一对一关系?
我很确定在 SQL Server 中建立真正的 1 对 1 关系在技术上是不可能的,因为这意味着你必须同时插入两条记录(否则你会在插入时遇到约束错误),在两个表,两个表之间都有外键关系。
话虽如此,使用外键描述的数据库设计是 1 到 0..1 的关系。没有可能需要 tableB 中的记录的约束。您可以与在 tableB 中创建记录的触发器建立伪关系。
所以有一些伪解决方案
首先,将所有数据存储在一个表中。那么你在EF中就没有问题了。
或者其次,您的实体必须足够聪明,除非它具有关联的记录,否则不允许插入。
或者第三,最有可能的是,你有一个你试图解决的问题,你问我们为什么你的解决方案不起作用,而不是你试图解决的实际问题(一个 XY 问题)。
更新
为了在现实中解释1 对 1 关系如何不起作用,我将使用鸡或蛋困境的类比。我不打算解决这个困境,但是如果你有一个约束,说为了向 Egg 表中添加一个 Egg,Chicken 的关系必须存在,并且 Chicken 必须存在于表中,那么你不能在 Egg 表中添加一个 Egg。反之亦然。如果没有与 Egg 的关系以及 Egg 表中存在的 Egg,则无法将 Chicken 添加到 Chicken 表。因此,在不破坏规则/约束之一的情况下,不能在数据库中创建每条记录。
一对一关系的数据库命名具有误导性。我见过的所有关系(因此我的经验)将更具描述性作为一对(零或一)关系。
将外键设置为主键,然后在两个主键字段上设置关系。而已!您应该在关系线的两端看到一个关键标志。这代表一对一。
这可以通过创建一个简单的主外键关系并以下列方式将外键列设置为唯一来完成:
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];
现在一般在Primary Foreign Relations(一对多)中,可以多次输入EmployeeID
,但是这里会报错
INSERT INTO [Salary] (
[EmployeeID]
, [SalaryAmount]
)
VALUES
(1, 3000);
上面的语句将显示错误为
违反 UNIQUE KEY 约束“UQ__Salary__7AD04FF0C044141D”。无法在对象“dbo.Salary”中插入重复键。重复键值为 (1)。
有一种方法我知道如何在不使用触发器、计算列、附加表或其他“奇异”技巧(只有外键和唯一约束)的情况下实现严格的*一对一关系,但有一点需要注意。
我将从公认的答案中借用鸡和蛋的概念来帮助我解释警告。
事实上,必须先有鸡或先有蛋(无论如何在当前的数据库中)。幸运的是,这个解决方案没有政治化,也没有规定哪个必须先出现 - 它由实施者决定。
需要注意的是,从技术上讲,允许记录“先行”的表可以创建一个记录,而另一个表中没有相应的记录;然而,在这个解决方案中,只允许一个这样的记录。当仅创建一条记录(仅创建 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
要插入,首先必须插入一个鸡蛋(鸡为空)。现在,只能插入一只鸡,并且它必须引用“无人认领”的鸡蛋。最后,可以更新添加的鸡蛋,它必须引用“无人认领”的鸡。在任何时候都不能让两只鸡引用同一个鸡蛋,反之亦然。
要删除,可以遵循相同的逻辑:将egg's Chicken 更新为null,删除新“无人认领”的鸡,删除egg。
该解决方案还允许轻松交换。有趣的是,交换可能是使用这种解决方案的最有力论据,因为它具有潜在的实际用途。通常,在大多数情况下,只需将两张表重构为一张,就能更好地实现两张表的一对一关系;但是,在潜在的情况下,这两个表可能代表真正不同的实体,它们需要严格的一对一关系,但需要经常交换“合作伙伴”或重新排列,同时仍保持一对一-重新安排后的一段关系。如果使用更常见的解决方案,则必须更新/覆盖其中一个实体的所有数据列,以便重新排列所有对,与此解决方案相反,
好吧,这是我使用标准约束所能做的最好的事情(不要评判:) 也许有人会发现它很有用。
SQL中的1对1关系是通过将两个表的字段合并为一个来建立的!
我知道您可以将表拆分为具有 1 对 1 关系的两个实体。大多数时候你使用它是因为你想在“表中二进制数据的重字段”上使用延迟加载。
示例:您有一个包含图片的表格,其中包含名称列(字符串)、可能是一些元数据列、缩略图列和图片本身 varbinary(max)。在您的应用程序中,您肯定会首先在集合控件中仅显示名称和缩略图,然后仅在需要时才加载“完整图片数据”。
如果这是您正在寻找的。这就是所谓的“表拆分”或“水平拆分”。
https://visualstudiomagazine.com/articles/2014/09/01/splitting-tables.aspx
如何在 SQL Server 中创建一对一关系?
简短的回答:你不能。
长答案:你可以,如果你敢继续阅读......
我知道当 DBMS ( *cough* MS SQL Server *cough* )不支持可延迟约束时,有两种主要方法可以“实现”1:1 关系。这篇文章讨论了这两种主要方法。
这两种方法都通过欺骗 EF 将 aVIEW
视为 a与 EF 具有一定程度的兼容性TABLE
。如果您不使用 EF,那么您可能不需要这些VIEW
对象,但它们对于方便查询和快速查询单独表中实体的产品类型视图仍然很方便。1:1
这两种方法都是围绕使用另一个仅ValidCountries
包含PK 值的表 ( ) 构建的,存在两个原因:
1:1
成员表都有 FK 约束(不要忘记您也可以有三个或更多表!):因此,除非所有必需的相关数据都存在于各自的表中,否则1:1
行ValidCountries
不能存在。FOREIGN KEY
约束提供目标。这将在下面更详细地解释和演示。1:1
这两种方法的不同之处在于它们对成员表的约束、它们对TRIGGER
对象的使用以及它们与 EF 的兼容性。我确信这两种方法的更多变化是可能的 - 这实际上取决于您如何建模数据和您的业务需求。
这些方法都没有使用CHECK CONSTRAINT
带有 UDF 的规则来验证其他表中的数据,这是目前实现1:1
约束的主要方法,但这种方法在性能方面的声誉很差。
TABLE
对象(一个用于前向声明,另一个作为有效性证明)和一个读/写VIEW
来仅公开1:1
来自 a 的有效数据JOIN
:此方法使用第三个表仅“前向声明” (共享)PK 值,而其他希望1:1
彼此建立关系的表仅引用前向声明表。
另一个“final”TABLE
用于证明(通过 FK 约束)对于任何给定的 PK,valid肯定存在。
然后,这种复杂性隐藏在一个(技术上可选的)VIEW
对象后面,该对象只公开有效数据并执行INNER JOIN
3 个(或更多)后备表中的一个,同时还支持INSERT/UPDATE/DELETE/MERGE
DML 操作。
VIEW
是TABLE
. 需要注意的是,所有这些方法都是严格的数据库优先的,因为所有这些方法都比 EF 更聪明,可以让其屈服于我们的意愿(所以一定要禁用迁移!)VIEW
它永远不会暴露无效数据,但实际上非常有必要作为来自其他单独实体表(绝不能引用前向声明表)的传入外键引用的目标.
VIEW
对象(甚至索引视图)不能参与 RDBMS 外键约束,这很烦人。这三个表是:
Countries
和Capitals
),这将是一个名为 like CountryDeclarations
(或CountryDecl
简称)的表,并且只 CountryName
存储值,这是 theCountries
和Capitals
表的共享 PK)。TABLE Countries
与CountryName
作为表的 PK及其FK 仅对前向声明表。TABLE Capitals
与CountryName
作为表的 PK及其FK 仅对前向声明表。TABLE ValidCountries
使用 PK + FK to并将列与和CountryDecl
分开。FK
Countries
Capitals
这是这种方法的数据库图:
从Countries
和/或Capitals
表中查询数据时,只要您始终提供INNER JOIN
,ValidCountries
您就会得到硬保证,即您始终在查询有效数据。
VIEW
来JOIN
为您完成已经完成的工作。请记住,成分表和表之间1:1
的关系不是强制的:这是必要的,否则会出现鸡与蛋的问题。Countries
Capitals
INSERT
INSERT
进入Countries
before Capitals
(并且DELETE
以相反的顺序)你可以添加一个FK
约束 fromCapitals
直接 to Countries
,但这并没有真正增加任何好处,因为该Countries
表不能保证相应的Capitals
行将存在。这种设计也与IDENTITY
PK 兼容,只要记住只有前向声明表会有IDENTITY
列,所有其他表都会有普通的int
PK+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. */
CREATE TABLE
.. .) 基于您的 Code-First 实体模型类的语句。
虽然 EF 不再支持“数据库优先”模型,但您仍然可以使用“数据库中的代码优先”和代码优先代码生成,例如https://github.com/sjh37/EntityFramework-Reverse-POCO-Code- First-Generator(免责声明:这是我个人最喜欢的代码生成器,我是该项目的贡献者)。
如果您使用这种方法在数据库上运行默认脚手架或代码优先代码生成,那么您最终会得到一个包含app1.Countries
单独实体的app1.Capitals
模型不想在你的 EF 模型中。app1.CountriesDecl
app1.ValidCountries
app1.*
从 EF 中排除所有表,而是指示 EF 将VIEW dbo.Countries
其视为单个实体(这是有道理的,因为从数学上讲,两个实体之间的每个 1:1
关系都与定义为产品类型的单个实体相同2 个其他实体)。VIEW
没有 aPRIMARY KEY
也没有任何FOREIGN KEY
约束,EF(默认情况下)不能正确地从 a 代码生成实体类VIEW
,但是前面提到的代码生成工具可以很容易地以正确的方式轻推 EF(查找下面的ViewProcessing
方法和AddForeignKeys
方法它)。如果您确实将app1.Countries
andapp1.Capitals
表保留为 EF 中的实体类型,请注意让 EF 执行INSERT
into 这两个表将失败,除非您的代码首先执行INSERT
into app1.CountriesDecl
。
或者您可以添加一个CREATE TRIGGER Countries/Capitals_Insert ON app1.Countries/app1.Capitals INSTEAD OF INSERT
将执行IF NOT EXIST ... INSERT INTO app1.CountriesDecl
.
但是,至少 EF 在这两张桌子上UPDATE
不会有任何问题。DELETE
TABLE
对象,但FK
列是NULL
-able - 并且 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`.
}
考虑到这个概念,我们可以拥有TABLE
包含部分/不完整的对象Country
和Capital
我们可以自由插入/更新/删除的数据,因为它们的相互FOREIGN KEY
约束是NULL
-able,见下文。
dbo.CountriesData
anddbo.CapitalsData
而不是dbo.Countries
和dbo.Capitals
分别表示这些表只包含任意“数据”而不是有效和正确的实体。这是我的个人命名约定。YMMV。VIEW dbo.Countries
exists 仅将有效实体公开为单一产品类型。
VIEW
对象,并让 EF 也将它们视为实体(尽管您需要做更多的INSERT
工作才能单独为每个视图工作)。但与方法 1不同的是,该dbo.CapitalsData
表现在有一个复合主键,这是 OP 特定数据库设计目标的结果——这可能不适用于您的数据库。
dbo.Countries
非NULL
CountryName
值。这是必须的,因为也是PK的,所以不能。这是因为 SQL Server 仅在 FK 中的所有列都为非. 如果您有不同的 PK 设计,那么这对您来说会有所不同。FK_CountriesData_to_Capitals
CountryName
dbo.CountriesData
NULL
NULL
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;
INSERT
一个新的国家...
INSERT INTO dbo.CountriesData
是初始NULL
CapitalName
值。NULL
.INSERT INTO dbo.CapitalsData
(反之亦然,提供CountryName
相反的情况NULL
)。UPDATE dbo.CountriesData SET CapitalName = inserted.CapitalName WHERE CountryName = inserted.CountryName
.VIEW dbo.Countries
现在将公开 now-valid1:1
相关数据。DELETE
必须以相反的顺序执行操作(即首先UPDATE
清除 FK,然后DELETE
以任何顺序从每个表中清除)。UPDATE
操作不需要特殊处理。INSERT
逻辑移动到和表AFTER INSERT
上的触发器中,因为这意味着:
CountriesData
CapitalsData
UPDATE
成AFTER INSERT
触发器就dbo.CapitalsData
!(反之亦然) - 但一定要添加检查WHERE inserted.CountryName IS NOT NULL
- 但如果你这样做,那么你的客户的 SQL 代码只需要执行两个INSERT
语句,两个AFTER INSERT
触发器之一将自动处理其余的,但前提是数据最终有效 - 因此它将在VIEW dbo.Countries
.CountriesDecl
单独INSERT
操作不会失败 - 但请记住,这两个表格/实体之间没有关系。dbo.CountriesData
dbo.CapitalsData
1:1
实现此目的的最简单方法是仅创建 1 个表,其中表 A 和 B 字段均不为空。这样就不可能有一个没有另一个。
那这个呢 ?
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)
)