C#中的类链接最佳实践

15

首先,EF不是我们开发环境的选项,所以请不要提供“只需使用EF”答案...

我认为这是一个相当标准的困境,所以我相信大多数专业人员都有一种方法来解决它,我只是还没有碰巧找到...所以我希望你们能够向我展示它是什么。

假设您有以下数据库表:

tblCompanies 
ID 
NAME

tblDepartments 
ID 
COMPANY_ID 
NAME

tblEmployees
ID 
DEPARTMENT_ID 
FIRSTNAME 
LASTNAME

如何在代码的类中最好地表示这个问题?

我认为最好的方式是这样的:

public class Company
{
     public int ID { get; set; }
     public string Name { get; set; }
     public List<Department> Departments { get; set; }
}

public class Department
{
     public int ID { get; set; }
     public string Name { get; set; }
     public List<Employee> Employees { get; set; }
}

public class Employee
{
     public int ID { get; set; }
     public string FirstName { get; set;}
     public string LastName { get; set; }
}

我认为这是"面向对象编程的正确方法",然而经常发生以下情况:

public class Department
{
     public int ID { get; set; }
     public string Name { get; set; }
     public int CompanyID { get; set; }
     public List<Employee> Employees { get; set; }
}

主要是因为当你从数据库中仅获取部门时,你只会得到公司ID,而没有其他属性来完整填充公司类的实例。

(这里我使用了一个相对简单的例子,但我目前处理的项目有3个字段用于将数据链接在一起,所以在几个类中拥有相同的3个字段对我来说似乎是错误的想法)

对于这些情况,是否有最佳实践?虽然我不喜欢因懒惰而在多个类中存储相同的数据,但我也不喜欢返回只填充了一个字段的类实例,因为那是我当时拥有的全部信息。


我相信你想要的“最佳方式”例子是你想要有一个 Company DepartmentCompany {get; set;} ? - Paolo Falabella
2
在你的一对多关系中,看起来你只代表了“多”的部分。例如,DepartmentEmployees,但是Employee没有DepartmentEmployee应该有一个Department属性,这样你就可以轻松地获取员工所在的部门。 - Makotosan
6个回答

5
这是一个普遍的问题,ORM试图解决这个问题。根据你的需求和约束条件,确保这不是一个容易的问题。
只有两个基本选项来保留一份信息副本。按需惰性加载数据或一开始就加载所有数据(贪婪加载)。否则,您必须复制数据。
通过懒加载,您基本上设置了这样的东西:当导航到属性时,您调用数据库并获取所需的信息以加载表示您正在访问的属性的实体。需要注意的棘手部分是选择N + 1问题。当您在一组父实体上进行迭代并触发每个子实体的延迟加载时,就会遇到此问题,从而导致调用数据库以加载一组实体(1)及其子级(N)的N+1次调用。
贪婪加载基本上意味着一开始加载您需要的所有内容。 ORM(如果它们工作)很好,因为它们通过LINQ处理许多细节,并创建可执行和可维护的解决方案,通常还允许您操纵贪婪和懒惰加载的使用。
另一个重要的陷阱是多对多关系。您需要确保没有循环初始化,并获得所有循环依赖项的负担。我肯定错过了许多其他要点。
在我看来,我不确定是否有一个最佳实践,只有一些实践是好的-没有什么是完美的。您可以:
1. 开始滚动自己的对象关系映射器,从而允许您摆脱重复的ID。 2. 使用较轻的ORM框架来处理某些内容,从而允许您摆脱重复的ID。 3. 创建专门的查询以加载数据聚合,从而允许您摆脱重复的ID(*咳嗽* DDD)。 4. 像上面提到的那样保留ID的重复,并不担心在域中创建显式关系模型。
这取决于您的约束条件,由您选择最佳方案。这是一个深入的话题,我的经验有限...所以请谨慎对待我的话。

1
非常好的答案...如果我可以选择多个答案,我也会选择这个。 - cavillac

3

我认为没有针对这种事情的“最佳实践”手册,而且肯定取决于你的课程将如何使用。但根据我的个人经验,我最终采用了以下方法:

public class Company
{
   public int ID { get; set; }
   public string Name { get; set; }

   public IEnumerable<Department> GetDepartments()
   {
      // Get departments here
   }
}

public class Department
{
   public int ID { get; set; }
   public string Name { get; set; }
   protected int CompanyID { get; set; }

   private Company _Company;
   public Company Company
   {
      get
      {
         // Get company here
      } 
   }

   public IEnumberable<Employee> GetEmployees()
   {
      // Get employees here
   }
}

public class Employee
{
   public int ID { get; set; }
   public string Name { get; set; }
   protected int DepartmentID { get; set; }

   private Department _Department;
   public Department Department
   {
      get
      {
         // Get department here
      } 
   }

   public IEnumberable<Employee> GetEmployees()
   {
      // Get employees here
   }
}

在某些情况下,我将我的类的一些“导航”属性(如CompanyID和DepartmentID)暴露为public,以防止实例化新类来获取已经加载的值。
正如其他人所指出的,你也可以模拟“延迟加载”,但这将需要额外的努力。

我已经做过类似于这个懒加载变体的事情...而且那可能真的是最好的方法。我只是不确定这是否是大多数人采用的标准解决方案,但是,这种方法可能与ORM如何处理数据最相似,而ORM似乎是当前的流行趋势,这可能可以解释这一点。 - cavillac

2

我认为这取决于需求。您是否需要向上遍历(从部门获取公司,从员工获取部门等)?如果是这样,最好提供一种方法来实现。理想情况下,这将是类似于公司或部门属性的东西,当然,您不希望获取您不真正需要的数据,因此您可能会保留一个私有公司ID,并具有公共getCompany函数,该函数查询数据。


由于某种原因,我从未考虑过隐藏ID。当然,他们随后必须使用“getCompany”来获取任何东西……即使只是获取ID。如果公司是按照延迟加载为目的构建的,那么它将是两全其美的选择,因为可以无需额外工作检索Department.Company.ID,还可以检索Department.Company.Name,但内置了数据调用。对于存储和隐藏以备将来使用仍然是个好主意。 - cavillac
实际上,无论哪种方式,您都会与数据库进行交互。您可以考虑您所提到的是更渴望加载而不是懒惰加载。在加载它时,还没有人要求它(可能永远不会)。另一方面,如果直到被要求才加载,那么当他们需要数据时就会有延迟。权衡是数据库请求和函数处理时间。如果您愿意冒db的风险,那么最好异步执行它(因为您还不需要这些数据)。 - aepheus

1

我认为这不是一个真正的面向对象编程问题,在您的情况下,您只有一个数据库模型(类中的数据库表示),它不包含任何逻辑,所有的类都被用作结构体,这是将数据库映射到类 - 结构体的正确方式。因此,在您的下一个模块中,它将代表程序的逻辑,您必须将数据库模块映射到真正的类中,这些类将包含逻辑(我的意思是实现它的方法),当然,如果您确实需要它们。因此,在我看来,面向对象编程问题应该在应用程序的逻辑部分。另一方面,您可以查看nhibernate以及其中的映射方式,这将为您提供最佳数据库模型实现的提示。


0

我相信这就是你在NHibernate中的类所看起来的样子:

public class Company
{
     public int ID { get; set; }
     public string Name { get; set; }
     public IList<Department> Departments { get; set; }
}

public class Department
{
     public int ID { get; set; }
     public string Name { get; set; }
     public Company Company { get; set; }
     public IList<Employee> Employees { get; set; }
}

public class Employee
{
     public int ID { get; set; }
     public string FirstName { get; set;}
     public string LastName { get; set; }
     public Department Department { get; set; }
}

请注意,有一种方法可以从员工导航到部门,从部门导航到公司(除了您已经指定的内容)。
NHibernate具有各种功能,使其正常工作。而且它非常好用。主要技巧是运行时代理对象,以允许延迟加载。此外,NHibernate支持许多不同的方式来急切和延迟加载,以完全按照您想要的方式进行加载。
当然,您可以在没有NHibernate或类似ORM的情况下获得相同的功能,但为什么不使用功能丰富的主流技术,而不是手动编写自己的功能较少的自定义ORM呢?

我没有研究过NHibernate,但我确实研究了EF,发现在我们办公室的编程环境中,无法找到一种有效的实现该技术的方法。我以前问过一个问题,基本上是关于“有没有办法我们可以实现EF?”但我并没有得到关于如何在我们的办公室实施它的答案。在asp.net论坛上也是同样的情况。 - cavillac
就个人而言,在许多情况下,数据库的结构并非由应用程序(或实用程序)开发人员设计,因此DB中的数据结构不易映射到ORM。 - Andrew Hanlon

0
还有另一种选择。创建一个“DataController”类来处理对象的加载和“记忆化”。dataController维护一个字典,其中包含[CompanyIDs,Company objects]和[DepartmentIDs,Department objects]。当您加载新的Department或Company时,您在此DataController字典中保留记录。然后,当您实例化新的Department或Employee时,您可以直接设置对父对象的引用,或者您可以使用Lazy [Company / Department]对象,并使用lambda(在构造函数中)设置它,这将维护DataController范围而不直接引用对象。我忘了提到一件事,您还可以在Dictionaries的getter / get方法中放置逻辑,以查询数据库是否找不到特定ID。将所有这些内容结合在一起,使您的类(模型)非常干净,同时仍然相当灵活,以便在何时/如何加载其数据。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接