一对零或一的关系

dha*_*ech 5 ihp

介绍

\n

为了测试 IHP,我已将Contoso University ASP.NET Core 教程的部分内容转换为 IHP。

\n

本教程中的这一步显示了数据模型图。根据该页面,我在这个问题中重点关注的部分涉及Instructor和 ,OfficeAssignment其中 和 具有一对零或一的关系。

\n

在此输入图像描述

\n

讲师

\n

C# 中的模型Instructor是:

\n
public class Instructor\n{\n    public int ID { get; set; }\n\n    [Required]\n    [Display(Name = "Last Name")]\n    [StringLength(50)]\n    public string LastName { get; set; }\n\n    [Required]\n    [Column("FirstName")]\n    [Display(Name = "First Name")]\n    [StringLength(50)]\n    public string FirstMidName { get; set; }\n\n    [DataType(DataType.Date)]\n    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]\n    [Display(Name = "Hire Date")]\n    public DateTime HireDate { get; set; }\n\n    [Display(Name = "Full Name")]\n    public string FullName\n    {\n        get { return LastName + ", " + FirstMidName; }\n    }\n\n    public ICollection<Course> Courses { get; set; }\n    public OfficeAssignment OfficeAssignment { get; set; }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这在 sqlite 中产生了下表:

\n
CREATE TABLE IF NOT EXISTS "Instructor" (\n    "ID" INTEGER NOT NULL CONSTRAINT "PK_Instructor" PRIMARY KEY AUTOINCREMENT,\n    "LastName" TEXT NOT NULL,\n    "FirstName" TEXT NOT NULL,\n    "HireDate" TEXT NOT NULL\n);\n
Run Code Online (Sandbox Code Playgroud)\n

所以在 IHP 中,我使用了以下内容:

\n
CREATE TABLE instructors (\n    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,\n    last_name TEXT NOT NULL,\n    first_mid_name TEXT NOT NULL,\n    hire_date DATE NOT NULL\n);\n
Run Code Online (Sandbox Code Playgroud)\n

办公室分配

\n

C# 中的模型OfficeAssignment是:

\n
public class Instructor\n{\n    public int ID { get; set; }\n\n    [Required]\n    [Display(Name = "Last Name")]\n    [StringLength(50)]\n    public string LastName { get; set; }\n\n    [Required]\n    [Column("FirstName")]\n    [Display(Name = "First Name")]\n    [StringLength(50)]\n    public string FirstMidName { get; set; }\n\n    [DataType(DataType.Date)]\n    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]\n    [Display(Name = "Hire Date")]\n    public DateTime HireDate { get; set; }\n\n    [Display(Name = "Full Name")]\n    public string FullName\n    {\n        get { return LastName + ", " + FirstMidName; }\n    }\n\n    public ICollection<Course> Courses { get; set; }\n    public OfficeAssignment OfficeAssignment { get; set; }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这在 sqlite 中产生了下表:

\n
CREATE TABLE IF NOT EXISTS "OfficeAssignment" (\n    "InstructorID" INTEGER NOT NULL CONSTRAINT "PK_OfficeAssignment" PRIMARY KEY,\n    "Location" TEXT NULL,\n    CONSTRAINT "FK_OfficeAssignment_Instructor_InstructorID" FOREIGN KEY ("InstructorID") REFERENCES "Instructor" ("ID") ON DELETE CASCADE\n);\n
Run Code Online (Sandbox Code Playgroud)\n

所以在 IHP 中,我使用了以下内容:

\n
CREATE TABLE office_assignments (\n    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,\n    instructor_id UUID NOT NULL,\n    "location" TEXT NOT NULL\n);\nCREATE INDEX office_assignments_instructor_id_index ON office_assignments (instructor_id);\nALTER TABLE office_assignments ADD CONSTRAINT office_assignments_ref_instructor_id FOREIGN KEY (instructor_id) REFERENCES instructors (id) ON DELETE NO ACTION;\n
Run Code Online (Sandbox Code Playgroud)\n

列差异

\n

请注意,ASP.NET Core 版本的表OfficeAssignment仅具有以下列:

\n
InstructorID\nLocation\n
Run Code Online (Sandbox Code Playgroud)\n

而 IHP 表有:

\n
id\ninstructor_id\nlocation\n
Run Code Online (Sandbox Code Playgroud)\n

即它有一个id列。我将其留在那里,因为它是由 IHP 架构编辑器默认添加的。

\n

一对零或一的关系

\n

Instructor给出in的生成代码Types.hs

\n
CREATE TABLE IF NOT EXISTS "Instructor" (\n    "ID" INTEGER NOT NULL CONSTRAINT "PK_Instructor" PRIMARY KEY AUTOINCREMENT,\n    "LastName" TEXT NOT NULL,\n    "FirstName" TEXT NOT NULL,\n    "HireDate" TEXT NOT NULL\n);\n
Run Code Online (Sandbox Code Playgroud)\n

IHP 的解释似乎是这样的:

\n
    \n
  • 一名讲师可以承担多项办公室任务
  • \n
\n

(即该字段officeAssignments是复数。)

\n

但是,根据 ASP.NET Core 教程中的图表,讲师可以有 0 或 1 个办公室任务。(即他们是否有办公室。)

\n

Entity Framework Core 似乎从 Instructor 上存在以下导航属性得到了这样的信号:每位讲师最多应有一个办公室分配:

\n
public OfficeAssignment OfficeAssignment { get; set; }\n
Run Code Online (Sandbox Code Playgroud)\n

更新:这已得到证实。请参阅下面标题为“更新 1”的部分。

\n

所需的语义 - 创建具有办公室的讲师

\n

在 C# 应用程序中,假设我创建一个Instructor,指定一个办公室:

\n

在此输入图像描述

\n

我们在sqlite中看到以下内容:

\n
sqlite> SELECT * FROM Instructor; SELECT * FROM OfficeAssignment;\nID  LastName     FirstName  HireDate\n--  -----------  ---------  -------------------\n1   Fakhouri     Fadi       2002-07-06 00:00:00\n2   Harui        Roger      1998-07-01 00:00:00\n3   Kapoor       Candace    2001-01-15 00:00:00\n4   Zheng        Roger      2004-02-12 00:00:00\n5   Abercrombie  Kim        1995-03-11 00:00:00\n10  Curry        Haskell    1920-01-01 00:00:00\nInstructorID  Location\n------------  ------------\n2             Gowan 27\n3             Thompson 304\n10            Haskell Room\n
Run Code Online (Sandbox Code Playgroud)\n

所需的语义 - 创建没有办公室的讲师

\n

现在,在 C# 应用程序中,我们创建一名讲师,但不指定办公室:

\n

在此输入图像描述

\n

我们在sqlite中看到以下内容:

\n
sqlite> SELECT * FROM Instructor; SELECT * FROM OfficeAssignment;\nID  LastName     FirstName  HireDate\n--  -----------  ---------  -------------------\n1   Fakhouri     Fadi       2002-07-06 00:00:00\n2   Harui        Roger      1998-07-01 00:00:00\n3   Kapoor       Candace    2001-01-15 00:00:00\n4   Zheng        Roger      2004-02-12 00:00:00\n5   Abercrombie  Kim        1995-03-11 00:00:00\n10  Curry        Haskell    1920-01-01 00:00:00\n11  Church       Alonzo     1940-01-01 00:00:00\nInstructorID  Location\n------------  ------------\n2             Gowan 27\n3             Thompson 304\n10            Haskell Room\n11\n
Run Code Online (Sandbox Code Playgroud)\n

有趣的是,如果我编辑讲师并将办公室留空:

\n

在此输入图像描述

\n

我们在sqlite中看到以下内容:

\n
sqlite> SELECT * FROM Instructor; SELECT * FROM OfficeAssignment;\nID  LastName     FirstName  HireDate\n--  -----------  ---------  -------------------\n1   Fakhouri     Fadi       2002-07-06 00:00:00\n2   Harui        Roger      1998-07-01 00:00:00\n3   Kapoor       Candace    2001-01-15 00:00:00\n4   Zheng        Roger      2004-02-12 00:00:00\n5   Abercrombie  Kim        1995-03-11 00:00:00\n10  Curry        Haskell    1920-01-01 00:00:00\n11  Church       Alonzo     1940-01-01 00:00:00\nInstructorID  Location\n------------  ------------\n2             Gowan 27\n3             Thompson 304\n
Run Code Online (Sandbox Code Playgroud)\n

即被OfficeAssignment删除。

\n

这段代码的实现是:

\n
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))\n                    instructorToUpdate.OfficeAssignment = null;\n
Run Code Online (Sandbox Code Playgroud)\n

问题

\n

这是在 IHP 端进行设置以对 C# 应用程序进行建模的好方法吗?或者我应该在 IHP 方面进行一些更改,以更紧密地模拟这种一对零或一的关系?

\n

我浏览了IHP 手册的关系部分,但没有注意到任何有关这种一对零或一关系的内容。只是想确保在我冒险进入表单方面之前我已经正确设置了模型。

\n

项目库

\n

如果有帮助,包含上述内容的项目存储库位于:

\n

https://github.com/dharmatech/ContosoUniversityIhp/tree/2021-09-04-02-queryOr-fix

\n

(这是非常混乱的,因为它是为了实验。)

\n

笔记

\n

我意识到这是一个复杂的问题,但我希望它可以作为人们将来在 IHP 中建立类似关系场景的示例。

\n

更新1

\n

Entity Framework Core 文档包含以下部分:

\n\n

它提到:

\n
\n

一对一关系双方都有参考导航属性。它们遵循与一对多关系相同的约定,但在外键属性上引入了唯一索引,以确保只有一个依赖项与每个主体相关。

\n
\n

这确实是我们在Instructor和 的C# 模型中看到的OfficeAssignment。所以我想问题是,IHP 是否明确支持这种关系?如果没有,在当前机制下模拟它的好方法是什么?

\n

可能的模型为Instructor

\n

似乎为了对Instructor他们可以拥有一个或零个办公室这一事实进行建模,生成的模型应该有一个如下所示的字段:

\n
officeAssignment :: Maybe OfficeAssignment\n
Run Code Online (Sandbox Code Playgroud)\n

如前所述,目前情况如下:

\n
data Instructor\' officeAssignments = Instructor {\n    id :: (Id\' "instructors"), \n    lastName :: Text, \n    firstMidName :: Text, \n    hireDate :: Data.Time.Calendar.Day, \n    officeAssignments :: officeAssignments, \n    meta :: MetaBag\n} deriving (Eq, Show)\n
Run Code Online (Sandbox Code Playgroud)\n

更新2

\n

如果我们看一下office_assignmentsIHP 方面的表格:

\n
CREATE TABLE office_assignments (\n    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,\n    instructor_id UUID NOT NULL,\n    "location" TEXT NOT NULL\n);\n
Run Code Online (Sandbox Code Playgroud)\n

很明显,由于有一列,所以对于给定的id,我们可以有任意数量的行。office_assignmentinstructor_id

\n

但是,如果我们看一下 C# 版本:

\n
CREATE TABLE IF NOT EXISTS "OfficeAssignment" (\n    "InstructorID" INTEGER NOT NULL CONSTRAINT "PK_OfficeAssignment" PRIMARY KEY,\n    "Location" TEXT NULL,\n    CONSTRAINT "FK_OfficeAssignment_Instructor_InstructorID" FOREIGN KEY ("InstructorID") REFERENCES "Instructor" ("ID") ON DELETE CASCADE\n);\n
Run Code Online (Sandbox Code Playgroud)\n

我们注意到:

\n
    \n
  • 没有id专栏。
  • \n
  • 只有一InstructorID列。
  • \n
  • InstructorID是个PRIMARY KEY
  • \n
  • OfficeAssignments因此,这似乎强制了这样一个事实:对于任何给定的只能有一行Instructor
  • \n
\n

因此,也许就像将 IHP 架构更改为一样简单:

\n
CREATE TABLE office_assignments (\n    instructor_id UUID PRIMARY KEY NOT NULL,\n    "location" TEXT NOT NULL\n);\n\nCREATE INDEX office_assignments_instructor_id_index ON office_assignments (instructor_id);\n\nALTER TABLE office_assignments ADD CONSTRAINT office_assignments_ref_instructor_id FOREIGN KEY (instructor_id) REFERENCES instructors (id) ON DELETE NO ACTION;\n
Run Code Online (Sandbox Code Playgroud)\n

结果

\n

好的,使用我更新的模式编辑器,office_assignments它现在看起来像这样:

\n
CREATE TABLE office_assignments (\n    instructor_id UUID PRIMARY KEY NOT NULL,\n    "location" TEXT NOT NULL\n);\n
Run Code Online (Sandbox Code Playgroud)\n

这是编译期间的结果:

\n
[ 4 of 23] Compiling Generated.Types  ( build/Generated/Types.hs, interpreted )\n\nbuild/Generated/Types.hs:133:71: error:\n    \xe2\x80\xa2 Couldn\'t match type \xe2\x80\x98"instructors"\xe2\x80\x99 with \xe2\x80\x98"office_assignments"\xe2\x80\x99\n        arising from a use of \xe2\x80\x98QueryBuilder.filterWhere\xe2\x80\x99\n    \xe2\x80\xa2 In the fifth argument of \xe2\x80\x98Instructor\xe2\x80\x99, namely\n        \xe2\x80\x98(QueryBuilder.filterWhere\n            (#instructorId, id) (QueryBuilder.query @OfficeAssignment))\xe2\x80\x99\n      In the expression:\n        Instructor\n          id lastName firstMidName hireDate\n          (QueryBuilder.filterWhere\n             (#instructorId, id) (QueryBuilder.query @OfficeAssignment))\n          def {originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord)}\n      In an equation for \xe2\x80\x98theRecord\xe2\x80\x99:\n          theRecord\n            = Instructor\n                id lastName firstMidName hireDate\n                (QueryBuilder.filterWhere\n                   (#instructorId, id) (QueryBuilder.query @OfficeAssignment))\n                def {originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord)}\n    |\n133 |         let theRecord = Instructor id lastName firstMidName hireDate (QueryBuilder.filterWhere (#instructorId, id) (QueryBuilder.query @OfficeAssignment)) def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }\n    |                                                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nFailed, three modules loaded.\n\nbuild/Generated/Types.hs:234:20: error:\n    \xe2\x80\xa2 Couldn\'t match type \xe2\x80\x98OfficeAssignment\' instructorId0\n                           -> instructorId0\xe2\x80\x99\n                     with \xe2\x80\x98Id\' "office_assignments"\xe2\x80\x99\n        arising from a use of \xe2\x80\x98QueryBuilder.filterWhere\xe2\x80\x99\n    \xe2\x80\xa2 In the second argument of \xe2\x80\x98(|>)\xe2\x80\x99, namely\n        \xe2\x80\x98QueryBuilder.filterWhere (#instructorId, instructorId)\xe2\x80\x99\n      In the expression:\n        builder |> QueryBuilder.filterWhere (#instructorId, instructorId)\n      In an equation for \xe2\x80\x98QueryBuilder.filterWhereId\xe2\x80\x99:\n          QueryBuilder.filterWhereId instructor_id builder\n            = builder |> QueryBuilder.filterWhere (#instructorId, instructorId)\n    |\n234 |         builder |> QueryBuilder.filterWhere (#instructorId, instructorId)\n    |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n
Run Code Online (Sandbox Code Playgroud)\n