首页 > 解决方案 > 一对零或一关系

问题描述

介绍

为了测试 IHP,我已将ASP.NET Core的Contoso 大学教程的一部分转换为 IHP。

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

在此处输入图像描述

讲师

C# 中的模型Instructor为:

public class Instructor
{
    public int ID { get; set; }

    [Required]
    [Display(Name = "Last Name")]
    [StringLength(50)]
    public string LastName { get; set; }

    [Required]
    [Column("FirstName")]
    [Display(Name = "First Name")]
    [StringLength(50)]
    public string FirstMidName { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Hire Date")]
    public DateTime HireDate { get; set; }

    [Display(Name = "Full Name")]
    public string FullName
    {
        get { return LastName + ", " + FirstMidName; }
    }

    public ICollection<Course> Courses { get; set; }
    public OfficeAssignment OfficeAssignment { get; set; }
}

这导致在 sqlite 中的下表:

CREATE TABLE IF NOT EXISTS "Instructor" (
    "ID" INTEGER NOT NULL CONSTRAINT "PK_Instructor" PRIMARY KEY AUTOINCREMENT,
    "LastName" TEXT NOT NULL,
    "FirstName" TEXT NOT NULL,
    "HireDate" TEXT NOT NULL
);

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

CREATE TABLE instructors (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    last_name TEXT NOT NULL,
    first_mid_name TEXT NOT NULL,
    hire_date DATE NOT NULL
);

办公室分配

C# 中的模型OfficeAssignment为:

public class OfficeAssignment
{
    [Key]
    public int InstructorID { get; set; }

    [StringLength(50)]
    [Display(Name = "Office Location")]
    public string Location { get; set; }

    public Instructor Instructor { get; set; }
}

这导致在 sqlite 中的下表:

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

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

CREATE TABLE office_assignments (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    instructor_id UUID NOT NULL,
    "location" TEXT NOT NULL
);
CREATE INDEX office_assignments_instructor_id_index ON office_assignments (instructor_id);
ALTER TABLE office_assignments ADD CONSTRAINT office_assignments_ref_instructor_id FOREIGN KEY (instructor_id) REFERENCES instructors (id) ON DELETE NO ACTION;

列差异

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

InstructorID
Location

而 IHP 表有:

id
instructor_id
location

即它有一个id专栏。我把它留在那里是因为它是由 IHP 模式编辑器默认添加的。

一对零或一关系

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

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

国际水文计划似乎在解释这样的事情:

(即字段officeAssignments是复数。)

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

由于 Instructor 上存在以下导航属性,Entity Framework Core 似乎得到了这样一个信号,即每位讲师最多应该有一个办公室任务:

public OfficeAssignment OfficeAssignment { get; set; }

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

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

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

在此处输入图像描述

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

sqlite> SELECT * FROM Instructor; SELECT * FROM OfficeAssignment;
ID  LastName     FirstName  HireDate
--  -----------  ---------  -------------------
1   Fakhouri     Fadi       2002-07-06 00:00:00
2   Harui        Roger      1998-07-01 00:00:00
3   Kapoor       Candace    2001-01-15 00:00:00
4   Zheng        Roger      2004-02-12 00:00:00
5   Abercrombie  Kim        1995-03-11 00:00:00
10  Curry        Haskell    1920-01-01 00:00:00
InstructorID  Location
------------  ------------
2             Gowan 27
3             Thompson 304
10            Haskell Room

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

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

在此处输入图像描述

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

sqlite> SELECT * FROM Instructor; SELECT * FROM OfficeAssignment;
ID  LastName     FirstName  HireDate
--  -----------  ---------  -------------------
1   Fakhouri     Fadi       2002-07-06 00:00:00
2   Harui        Roger      1998-07-01 00:00:00
3   Kapoor       Candace    2001-01-15 00:00:00
4   Zheng        Roger      2004-02-12 00:00:00
5   Abercrombie  Kim        1995-03-11 00:00:00
10  Curry        Haskell    1920-01-01 00:00:00
11  Church       Alonzo     1940-01-01 00:00:00
InstructorID  Location
------------  ------------
2             Gowan 27
3             Thompson 304
10            Haskell Room
11

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

在此处输入图像描述

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

sqlite> SELECT * FROM Instructor; SELECT * FROM OfficeAssignment;
ID  LastName     FirstName  HireDate
--  -----------  ---------  -------------------
1   Fakhouri     Fadi       2002-07-06 00:00:00
2   Harui        Roger      1998-07-01 00:00:00
3   Kapoor       Candace    2001-01-15 00:00:00
4   Zheng        Roger      2004-02-12 00:00:00
5   Abercrombie  Kim        1995-03-11 00:00:00
10  Curry        Haskell    1920-01-01 00:00:00
11  Church       Alonzo     1940-01-01 00:00:00
InstructorID  Location
------------  ------------
2             Gowan 27
3             Thompson 304

即被OfficeAssignment删除。

这段代码实现了:

if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
                    instructorToUpdate.OfficeAssignment = null;

问题

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

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

项目存储库

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

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

(它非常混乱,因为它是用于实验的。)

笔记

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

更新 1

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

它提到:

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

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

可能的模型Instructor

似乎为了Instructor模拟他们可以有一个或零个办公室的事实,生成的模型应该有一个像这样的字段:

officeAssignment :: Maybe OfficeAssignment

如前所述,目前如下:

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

更新 2

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

CREATE TABLE office_assignments (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    instructor_id UUID NOT NULL,
    "location" TEXT NOT NULL
);

很明显,既然有一个id列,我们可以office_assignment为给定的instructor_id.

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

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

我们注意到:

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

CREATE TABLE office_assignments (
    instructor_id UUID PRIMARY KEY NOT NULL,
    "location" TEXT NOT NULL
);

CREATE INDEX office_assignments_instructor_id_index ON office_assignments (instructor_id);

ALTER TABLE office_assignments ADD CONSTRAINT office_assignments_ref_instructor_id FOREIGN KEY (instructor_id) REFERENCES instructors (id) ON DELETE NO ACTION;

结果

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

CREATE TABLE office_assignments (
    instructor_id UUID PRIMARY KEY NOT NULL,
    "location" TEXT NOT NULL
);

这是编译期间的结果:

[ 4 of 23] Compiling Generated.Types  ( build/Generated/Types.hs, interpreted )

build/Generated/Types.hs:133:71: error:
    • Couldn't match type ‘&quot;instructors"’ with ‘&quot;office_assignments"’
        arising from a use of ‘QueryBuilder.filterWhere’
    • In the fifth argument of ‘Instructor’, namely
        ‘(QueryBuilder.filterWhere
            (#instructorId, id) (QueryBuilder.query @OfficeAssignment))’
      In the expression:
        Instructor
          id lastName firstMidName hireDate
          (QueryBuilder.filterWhere
             (#instructorId, id) (QueryBuilder.query @OfficeAssignment))
          def {originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord)}
      In an equation for ‘theRecord’:
          theRecord
            = Instructor
                id lastName firstMidName hireDate
                (QueryBuilder.filterWhere
                   (#instructorId, id) (QueryBuilder.query @OfficeAssignment))
                def {originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord)}
    |
133 |         let theRecord = Instructor id lastName firstMidName hireDate (QueryBuilder.filterWhere (#instructorId, id) (QueryBuilder.query @OfficeAssignment)) def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
    |                                                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, three modules loaded.

build/Generated/Types.hs:234:20: error:
    • Couldn't match type ‘OfficeAssignment' instructorId0
                           -> instructorId0’
                     with ‘Id' "office_assignments"’
        arising from a use of ‘QueryBuilder.filterWhere’
    • In the second argument of ‘(|>)’, namely
        ‘QueryBuilder.filterWhere (#instructorId, instructorId)’
      In the expression:
        builder |> QueryBuilder.filterWhere (#instructorId, instructorId)
      In an equation for ‘QueryBuilder.filterWhereId’:
          QueryBuilder.filterWhereId instructor_id builder
            = builder |> QueryBuilder.filterWhere (#instructorId, instructorId)
    |
234 |         builder |> QueryBuilder.filterWhere (#instructorId, instructorId)
    |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

标签: ihp

解决方案


推荐阅读