Jam*_*mes 40 sql sql-server entity-framework one-to-one
我想在SQL Server 2008 R2中创建一对一的关系.
我有两个表tableA
和tableB
,我设置tableB
的主键的外键引用其中tableA
的首要.但是当我首先使用Entity Framework数据库时,模型是1到0..1.
任何人都知道如何在数据库中创建真正的1对1关系?
提前致谢!
Pra*_*ana 60
将外键设置为主键,然后在两个主键字段上设置关系.而已!你应该在关系线的两端看到一个关键标志.这代表一对一.
Eri*_*ips 54
我很确定在SQL Server中技术上不可能有一个真正的1对1关系,因为这意味着你必须同时插入两个记录(否则你会在插入时遇到约束错误),两个表,两个表彼此具有外键关系.
话虽这么说,用外键描述的数据库设计是1到0..1的关系.没有约束可能需要tableB中的记录.您可以与触发器建立伪关系,以在tableB中创建记录.
所以有一些伪解决方案
首先,将所有数据存储在一个表中.那么你在EF中没有任何问题.
或者其次,您的实体必须足够聪明,不允许插入,除非它有相关记录.
或者第三,也是最有可能的,你有一个问题,你试图解决,而你问我们为什么你的解决方案不起作用而不是你想要解决的实际问题(一个XY问题).
UPDATE
为了解释现实中 1对1关系如何不起作用,我将使用鸡的类比或鸡蛋困境.我不打算解决这个难题,但是如果你有一个约束,为了在鸡蛋表中添加一个鸡蛋,必须存在鸡的关系,鸡必须存在于表中,然后你无法在Egg桌上添加一个Egg.反之亦然.如果没有与Egg表中存在的Egg和Egg的关系,则不能将Chicken添加到Chicken表中.因此,在不破坏其中一个规则/约束的情况下,不会在数据库中创建任何记录.
数据库一对一关系的命名是误导性的.我见过的所有关系(根据我的经验)将更具描述性,即一对一(零或一)关系.
小智 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)
我知道如何在不使用触发器、计算列、附加表或其他“奇异”技巧(仅外键和唯一约束)的情况下实现严格*一对一关系,但有一个小警告。
我将从接受的答案中借用鸡和蛋的概念来帮助我解释警告。
事实是必须先有鸡或有蛋(无论如何在当前的数据库中)。幸运的是,这个解决方案没有政治性,也没有规定哪个必须先出现——它留给了实施者。
需要注意的是,在技术上允许记录“先来”的表可以在没有其他表中相应记录的情况下创建记录;然而,在这个解决方案中,只允许一个这样的记录。当只创建一个记录(只有鸡或蛋)时,在删除“孤独”记录或在另一个表中创建匹配记录之前,不能向两个表中的任何一个添加更多记录。
解决方案:
向每个表添加外键,引用另一个,为每个外键添加唯一约束,并使一个外键可以为空,另一个不可为空,也是主键。为此,可空列上的唯一约束必须只允许一个空值(在 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 Server 中创建一对一关系?
简短的回答:你不能。
长答案:你可以,如果你敢继续读下去......
据我了解,当 DBMS 不支持可延迟约束时,有两种主要方法可以“实现”1:1 关系(*咳嗽* MS SQL Server *咳嗽*)。这篇文章讨论了这两种主要方法。
这两种方法都通过欺骗 EF 将 a 视为 来与 EF 具有一定程度的VIEW
兼容性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
彼此建立关系的其他表仅引用前向声明表。
另一个“最终”TABLE
用于证明(通过 FK 约束)对于任何给定的 PK,有效的肯定存在。
然后,这种复杂性隐藏在一个(技术上可选的)VIEW
对象后面,该对象仅公开有效数据并执行INNER JOIN
3 个(或更多)支持表中的一个,同时还支持INSERT/UPDATE/DELETE/MERGE
DML 操作。
VIEW
是TABLE
. 需要注意的是,所有这些方法都是严格数据库优先的,因为所有这些方法都比 EF更聪明,可以使其屈服于我们的意愿(因此请务必禁用迁移!)VIEW
永远不会暴露无效数据,但实际上非常有必要作为来自其他单独实体表的传入外键引用的目标(绝不能引用前向声明表) 。
VIEW
对象(甚至索引视图)无法参与 RDBMS 外键约束,这很烦人。这三个表是:
Countries
和Capitals
),这将是一个名为 like CountryDeclarations
(或CountryDecl
简称)的表,并且仅存 储值,这是和表CountryName
的共享 PK )。Countries
Capitals
TABLE Countries
withCountryName
作为表的 PK及其FK 仅用于前向声明表。TABLE Capitals
withCountryName
作为表的 PK及其FK 仅用于前向声明表。TABLE ValidCountries
PK + FK toCountryDecl
和分隔FK
列 toCountries
和Capitals
。这是此方法的数据库图:
当从Countries
和/或Capitals
表中查询数据时,只要始终提供INNER JOIN
,ValidCountries
那么您就可以得到始终查询有效数据的硬保证。
VIEW
工作。JOIN
请记住,成分和表之间的1:1
关系不是强制的:这是必要的,否则会出现先有鸡还是先有蛋的问题。Countries
Capitals
INSERT
INSERT
进入Countries
before Capitals
(并且DELETE
以相反的顺序),您可以直接添加一个FK
约束 from到,但这并没有真正增加任何好处,因为表无法保证相应的行将存在。Capitals
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. */
Run Code Online (Sandbox Code Playgroud)
CREATE TABLE
.. .) 基于您的 Code-First 实体模型类的语句。
虽然 EF 不再支持“数据库优先”模型,但您仍然可以将“代码优先从数据库”与代码优先代码生成一起使用,例如https://github.com/sjh37/EntityFramework-Reverse-POCO-Code- First-Generator(免责声明:这是我个人最喜欢的代码生成器,我是该项目的贡献者)。
如果您使用这种方法在数据库上运行默认脚手架或 code-first-codegen ,那么您最终会得到一个包含app1.Countries
、app1.Capitals
和app1.CountriesDecl
的单独实体的模型app1.ValidCountries
- 因此您应该配置您的 code-gen 以过滤掉那些您不需要的对象不希望出现在您的 EF 模型中。
app1.*
表,而是指示 EF 将其视为VIEW dbo.Countries
单个实体(这是有道理的,因为从数学上讲,两个实体之间的每个 关系都与定义为这些实体的产品类型的单个实体1:1
相同)2 个其他实体)。VIEW
没有 aPRIMARY KEY
也没有任何FOREIGN KEY
约束,所以 EF(默认情况下)无法从 a 正确地编码生成实体类VIEW
,但是前面提到的代码生成工具可以轻松地以正确的方式推动 EF(查找方法ViewProcessing
,以及AddForeignKeys
下面的方法)它)。如果您确实app1.Countries
将和表保留app1.Capitals
为 EF 中的实体类型,请注意,让 EFINSERT
对这两个表执行 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
.
UPDATE
然而,至少 EF 在这两个表上不会有任何问题DELETE
。
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
保存部分/不完整的对象Country
和Capital
可以自由插入/更新/删除的数据,因为它们的相互FOREIGN KEY
约束是NULL
可以的,见下文。
dbo.CountriesData
和 ,dbo.CapitalsData
而不是dbo.Countries
和dbo.Capitals
,以表明这些表仅包含任意“数据”而不是有效且正确的实体。这是我个人的命名约定。YMMV。VIEW dbo.Countries
实体公开为单一产品类型。
VIEW
分别为国家和首都定义其他对象,并让 EF 将它们也视为实体(尽管您需要加载更多的跑腿工作来INSERT
单独为每个视图进行工作)。但与方法 1不同的是,该dbo.CapitalsData
表现在具有复合主键,这是 OP 的特定数据库设计目标的结果 - 这可能不适用于您的数据库。
dbo.Countries
具有非值。这是必要的,因为也是PK的,所以不可能。这是可行的,因为 SQL Server 仅当FK 中的所有列都是非列时才强制执行 FK 约束。如果你有不同的PK设计,那么这对你来说将会有所不同。NULL
CountryName
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;
Run Code Online (Sandbox Code Playgroud)
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
现在将公开现在有效的1: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
归档时间: |
|
查看次数: |
115033 次 |
最近记录: |