【问题标题】:Express One-to-Many relationship in Entity Framework via join table通过连接表在实体框架中表达一对多关系
【发布时间】:2016-06-13 15:06:01
【问题描述】:

在数据库设计中,一种常见的做法是在“一”(或子)端用外键表示一对多关系。实体框架很好地处理了这种情况。

但是,我遇到了通过连接表表示一对多关系的情况,其中表上的两个外键之一具有唯一约束。

如何配置实体框架以使用此连接表?

在我目前的状态下,当对一个/子实体执行简单的读取查询时,Entity Frameworks 抛出异常——正如预期的那样——一个/子表缺少基于常规名称的列在导航属性上。

【问题讨论】:

  • 如果您有一个连接表.....那么(即使有 db 约束来保证 1:N),您必须将 EF 部分视为 M:N。您可以在 EF-Entity 上编写一个(仅获取)属性,该属性将带回 FirstOrDefault(),该属性将带回 1:N 中的“一个”。
  • @granadaCoder 噢,我喜欢这样。我会尝试并报告。
  • @granadaCoder 但它应该是一个只能获取的属性。它应该有一个将它添加到集合中的 setter。
  • 这也很好。只是让您知道 setter 不是一个简单的 setter。
  • @VD。在我的回答中,我将 Department to Employee 作为子表中的 FK。但是您也可以看到海报的设置与 Employee 到 ParkingArea 的关系。当“今天,我认为只有 1:N,但在未来某个时候我可以预见到 M:N”时,我会使用此规则......所以我继续编写代码以将其扩展到 M:N稍后。下面的 Employee to ParkingArea DDL 显示了这一点。

标签: c# entity-framework one-to-many jointable


【解决方案1】:

如果我有以下 DDL。

员工 (M:N) 到 ParkingArea。但是,一个约束在链接表中只保留一个 Employee,因此是 1:N。

-- 启动 TSQL

Use OrganizationReverseDB
GO


SET NOCOUNT ON


IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'[dbo].[EmployeeParkingAreaLink]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) 
BEGIN DROP TABLE [dbo].[EmployeeParkingAreaLink] 
END 
GO

IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'[dbo].[ParkingArea]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) 
BEGIN 
DROP TABLE [dbo].[ParkingArea] 
END 
GO

IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'[dbo].[Employee]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) 
BEGIN 
DROP TABLE [dbo].[Employee] 
END 


IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'[dbo].[Employee]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) 
BEGIN DROP TABLE [dbo].[Employee] 
END 


IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'[dbo].[Department]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) 
BEGIN DROP TABLE [dbo].[Department] 
END 

GO


CREATE TABLE [dbo].[Department](
    [DepartmentUUID] [uniqueidentifier] NOT NULL,
    [TheVersionProperty] [timestamp] NOT NULL,
    [DepartmentName] [nvarchar](80) NULL,
    [CreateDate] [datetime] NOT NULL,
    )


ALTER TABLE dbo.[Department] ADD CONSTRAINT PK_Department PRIMARY KEY NONCLUSTERED ([DepartmentUUID]) 
GO

ALTER TABLE [dbo].[Department] ADD CONSTRAINT CK_DepartmentName_Unique UNIQUE ([DepartmentName]) 
GO


CREATE TABLE [dbo].[Employee] ( 

    [EmployeeUUID] [uniqueidentifier] NOT NULL,
    [ParentDepartmentUUID] [uniqueidentifier] NOT NULL,
    [TheVersionProperty] [timestamp] NOT NULL,
    [SSN] [nvarchar](11) NOT NULL,
    [LastName] [varchar](64) NOT NULL,
    [FirstName] [varchar](64) NOT NULL,
    [CreateDate] [datetime] NOT NULL,
    [HireDate] [datetime] NOT NULL
    )

GO

ALTER TABLE dbo.Employee ADD CONSTRAINT PK_Employee PRIMARY KEY NONCLUSTERED (EmployeeUUID) 
GO


ALTER TABLE [dbo].[Employee] ADD CONSTRAINT CK_SSN_Unique UNIQUE (SSN) 
GO

ALTER TABLE [dbo].[Employee] ADD CONSTRAINT FK_EmployeeToDepartment FOREIGN KEY (ParentDepartmentUUID) REFERENCES dbo.Department (DepartmentUUID) 
GO





IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'[dbo].[ParkingArea]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) 
BEGIN DROP TABLE [dbo].[ParkingArea] 
END 
GO

CREATE TABLE [dbo].[ParkingArea] 
( 
ParkingAreaUUID [UNIQUEIDENTIFIER] NOT NULL DEFAULT NEWSEQUENTIALID() 
, ParkingAreaName varchar(24) not null 
, CreateDate smalldatetime not null
)

GO

ALTER TABLE dbo.ParkingArea ADD CONSTRAINT PK_ParkingArea PRIMARY KEY NONCLUSTERED (ParkingAreaUUID) 
GO

ALTER TABLE [dbo].[ParkingArea] ADD CONSTRAINT CK_ParkingAreaName_Unique UNIQUE (ParkingAreaName) 
GO



IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'[dbo].[EmployeeParkingAreaLink]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) 
BEGIN DROP TABLE [dbo].[EmployeeParkingAreaLink] 
END 
GO

CREATE TABLE [dbo].[EmployeeParkingAreaLink] ( 
    /* [LinkSurrogateUUID] [uniqueidentifier] NOT NULL, */
    [TheEmployeeUUID] [uniqueidentifier] NOT NULL,
    [TheParkingAreaUUID] [uniqueidentifier] NOT NULL
)


GO

/*
ALTER TABLE dbo.EmployeeParkingAreaLink ADD CONSTRAINT PK_EmployeeParkingAreaLink PRIMARY KEY NONCLUSTERED (LinkSurrogateUUID) 
*/
GO

ALTER TABLE [dbo].[EmployeeParkingAreaLink] ADD CONSTRAINT FK_EmployeeParkingAreaLinkToEmployee FOREIGN KEY (TheEmployeeUUID) REFERENCES dbo.Employee (EmployeeUUID) 
GO

ALTER TABLE [dbo].[EmployeeParkingAreaLink] ADD CONSTRAINT FK_EmployeeParkingAreaLinkToParkingArea FOREIGN KEY (TheParkingAreaUUID) REFERENCES dbo.ParkingArea (ParkingAreaUUID) 
GO

ALTER TABLE [dbo].[EmployeeParkingAreaLink] ADD CONSTRAINT CONST_UNIQUE_EmpUUID UNIQUE (TheEmployeeUUID ) 
GO

ALTER TABLE [dbo].[EmployeeParkingAreaLink] ADD CONSTRAINT CONST_UNIQUE_EmpUUID_PAUUID UNIQUE (TheEmployeeUUID , TheParkingAreaUUID) 
GO


Insert into dbo.Department ( [DepartmentUUID], [DepartmentName] , CreateDate )
    select '10000000-0000-0000-0000-000000000001' , 'DepartmentOne' , CURRENT_TIMESTAMP
    union all   select '10000000-0000-0000-0000-000000000002' , 'DepartmentTwo' , CURRENT_TIMESTAMP

Insert into dbo.Employee ( EmployeeUUID, SSN , CreateDate, HireDate , LastName, FirstName , ParentDepartmentUUID)
    select '20000000-0000-0000-0000-000000000001' , '111-11-1111' , CURRENT_TIMESTAMP , '01/31/2001' , 'Smith' , 'John' , '10000000-0000-0000-0000-000000000001'
    union all   select '20000000-0000-0000-0000-000000000002' , '222-22-2222' , CURRENT_TIMESTAMP, '02/28/2002' , 'Jones' , 'Mary' , '10000000-0000-0000-0000-000000000002'

Insert into dbo.ParkingArea ( [ParkingAreaUUID], [ParkingAreaName] , CreateDate )
    select '30000000-0000-0000-0000-000000000001' , 'ParkingAreaOne' , CURRENT_TIMESTAMP
    union all   select '30000000-0000-0000-0000-000000000002' , 'ParkingAreaTwo' , CURRENT_TIMESTAMP


INSERT INTO [dbo].[EmployeeParkingAreaLink] (   [TheEmployeeUUID] , [TheParkingAreaUUID] )
        Select '20000000-0000-0000-0000-000000000001' , '30000000-0000-0000-0000-000000000001'
union all       Select '20000000-0000-0000-0000-000000000002' , '30000000-0000-0000-0000-000000000002'

约束“CONST_UNIQUE_EmpUUID”是您所说的设置。

EmployeeEntity 像这样:

[Serializable]
public partial class EmployeeEFEntity
{

public EmployeeEFEntity()
{
    CommonConstructor();
}
private void CommonConstructor()
{
    //this.MyParkingAreas = new List<ParkingAreaEFEntity>();
}



//EF Tweaks
public virtual Guid? ParentDepartmentUUID { get; set; }

public virtual Guid? EmployeeUUID { get; set; }

public virtual byte[] TheVersionProperty { get; set; }

public virtual DepartmentEFEntity ParentDepartment { get; set; }

public virtual string SSN { get; set; }
public virtual string LastName { get; set; }
public virtual string FirstName { get; set; }
public virtual DateTime CreateDate { get; set; }
public virtual DateTime HireDate { get; set; }

public virtual ICollection<ParkingAreaEFEntity> MyParkingAreas { get; set; }

public ParkingAreaEFEntity MyOneParkingAreaEFEntity {

    get 
    {
        return MyParkingAreas.FirstOrDefault();
    }
    set
    {
        /* check for more than one here */
        this.AddParkingArea(pa);
    }
}


public virtual void AddParkingArea(ParkingAreaEFEntity pa)
{
    if (!pa.MyEmployees.Contains(this))
    {
        pa.MyEmployees.Add(this);
    }
    if (!this.MyParkingAreas.Contains(pa))
    {
        this.MyParkingAreas.Add(pa);
    }
}


public virtual void RemoveParkingArea(ParkingAreaEFEntity pa)
{
    if (pa.MyEmployees.Contains(this))
    {
        pa.MyEmployees.Remove(this);
    }
    if (this.MyParkingAreas.Contains(pa))
    {
        this.MyParkingAreas.Remove(pa);
    }
}


public override string ToString()
{
    return string.Format("{0}:{1},{2}", this.SSN, this.LastName, this.FirstName);
}

你会这样映射:

public class EmployeeMap : EntityTypeConfiguration<EmployeeEFEntity>
{
    public EmployeeMap()
    {
        // Primary Key
        this.HasKey(t => t.EmployeeUUID);

        this.Property(t => t.EmployeeUUID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

        // Properties
        this.Property(t => t.TheVersionProperty)
            .IsRequired()
            .IsFixedLength()
            .HasMaxLength(8)
            .IsRowVersion();

        this.Property(t => t.SSN)
            .IsRequired()
            .HasMaxLength(11);

        this.Property(t => t.LastName)
            .IsRequired()
            .HasMaxLength(64);

        this.Property(t => t.FirstName)
            .IsRequired()
            .HasMaxLength(64);

        // Table & Column Mappings
        this.ToTable("Employee");
        this.Property(t => t.EmployeeUUID).HasColumnName("EmployeeUUID");
        this.Property(t => t.ParentDepartmentUUID).HasColumnName("ParentDepartmentUUID");
        this.Property(t => t.TheVersionProperty).HasColumnName("TheVersionProperty");
        this.Property(t => t.SSN).HasColumnName("SSN");
        this.Property(t => t.LastName).HasColumnName("LastName");
        this.Property(t => t.FirstName).HasColumnName("FirstName");
        this.Property(t => t.CreateDate).HasColumnName("CreateDate");
        this.Property(t => t.HireDate).HasColumnName("HireDate");

        // Relationships
        this.HasMany(t => t.MyParkingAreas)
            .WithMany(t => t.MyEmployees)
            .Map(m =>
            {
                m.ToTable("EmployeeParkingAreaLink");
                m.MapLeftKey("TheEmployeeUUID");
                m.MapRightKey("TheParkingAreaUUID");
            });

        this.HasRequired(t => t.ParentDepartment)
            .WithMany(t => t.Employees)
            .HasForeignKey(d => d.ParentDepartmentUUID);

    }
}

这样的停车区:

[Serializable]
public partial class ParkingAreaEFEntity
{

    public ParkingAreaEFEntity()
    {
        CommonConstructor();
    }
    private void CommonConstructor()
    {
        //this.MyEmployees = new List<EmployeeEFEntity>();
    }

    public virtual Guid ParkingAreaUUID { get; set; }


    public virtual string ParkingAreaName { get; set; }
    public virtual DateTime CreateDate { get; set; }

    public virtual ICollection<EmployeeEFEntity> MyEmployees { get; set; }

    public virtual void AddEmployee(EmployeeEFEntity emp)
    {
        if (!emp.MyParkingAreas.Contains(this))
        {
            emp.MyParkingAreas.Add(this);
        }
        if (!this.MyEmployees.Contains(emp))
        {
            this.MyEmployees.Add(emp);
        }
    }

    public virtual void RemoveEmployee(EmployeeEFEntity emp)
    {
        if (emp.MyParkingAreas.Contains(this))
        {
            emp.MyParkingAreas.Remove(this);
        }
        if (this.MyEmployees.Contains(emp))
        {
            this.MyEmployees.Remove(emp);
        }
    }

【讨论】:

  • 虽然这个例子可以大大简化,但我认为它确实展示了你对 EF 的“谎言”的评论,并说连接名称是用于多对多关系的。我想向其他读者强调重要的部分......即,在 EmployeeMap 类(这是 EmployeeEFEntity 的流畅 EF 配置)中,HasMany.(-).WithMany(-).Map(-) 是我们向 EF “撒谎”并指定加入表。
  • 另外,在我的设置中(尽管我将在您的示例中使用命名),我需要添加配置 Ignore(x =&gt; x.MyOneParkingAreaEFEntity) 以便 EF 认为数据库中没有列姓名MyOneParkingAreaEFEntity_ID.
  • 这是我为 POC 保留的示例。一旦你解决了一个 1:N、一个 M:N(关系上没有属性)、一个 M:N(关系上有属性)和一个查找表......大多数事情都可以从中派生出来.
  • 对,就 EF 而言,MyOneParkingAreaEFEntity 必须被忽略。
【解决方案2】:

您正在寻找的是与连接表的可选一对多关联。这背后的动机是我们总是尽量避免关系数据库模式中的可空列。未知信息会降低您存储的数据的质量。因此,可选的实体关联,无论是一对一还是一对多,最好在带有连接表的 SQL 数据库中表示,以避免可为空的外键列。

话虽如此,但不幸的是,EF支持这种类型的映射。如果您真的想实现这一点,那么您可能想看看其他支持一对多关联的 ORM 框架与连接表(如 NHibernate)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-27
    • 1970-01-01
    • 2021-06-16
    • 2011-11-13
    • 2013-11-24
    相关资源
    最近更新 更多