【问题标题】:What is the most elegant way to update a child collection when using nhibernate (without creating unnecessary adds and deletes)?使用 nhibernate 时更新子集合的最优雅方法是什么(不创建不必要的添加和删除)?
【发布时间】:2014-08-09 12:28:59
【问题描述】:

我有一个名为 Project 的域对象,它映射到我的 SQL Server 数据库中的一个表。它有一个名为 Dependencies 的 List 属性。

   public class Project
   {
         public int Id;
         public List<ProjectDependency> Dependencies;   
   }

   public class ProjectDependency
   {
          public Project Project;
          public Project Dependency;
   }

并且我试图找出在给定新的dependencyId列表的情况下更新依赖项列表的最有效方法。

所以这是一个幼稚的实现:

 public void UpdateDependencies(Project p, List<int> newDependencyIds)
 {
       p.Dependencies.Clear();
       foreach (var dependencyId in newDependencyIds)
       {
             Project d = GetDependency(dependencyId)
             p.Dependencies.Add(new ProjectDependency{Project = p, Dependency = d});
       }
 }

但这里的问题是,即使没有任何变化,我也会清除所有项目并插入之前存在的相同项目。

我正在寻找一种优雅的方式来确定差异(添加了什么,删除了什么)并进行这些更改,因此如果之前和之后存在依赖项,那么它就不会被触及。

【问题讨论】:

    标签: c# arrays list nhibernate


    【解决方案1】:
        public void UpdateDependencies(Project p, List<int> newDependencyIds)
        {
            p.Dependencies.RemoveAll(d => !newDependencyIds.Contains(d.Dependency.Id));
            var toAdd = newDependencyIds.Select(d => p.Dependencies.Any(pd => pd.Dependency.Id != d)).ToList();
            toAdd.ForEach(dependencyId => p.Dependencies.Add(new ProjectDependency{Project = p, Dependency = GetDependency(dependencyId)}));
        }
    

    【讨论】:

    • 在您回复的最后一行,我收到错误消息说无法添加 lambda 表达式。我觉得逻辑有问题
    • 我认为有一个错字,dependencyId => 应该紧跟在 ForEach(
    【解决方案2】:

    我真的对你的Model 课程感到困惑。您正在创建一个类Project。

    public class Project
    {
         public int Id;
         ...  
    }
    

    它本身在另一个类中被引用-

    public class ProjectDependency
    {
        public Project Project;
        public Project Dependency;
    }
    

    您又是在引用类中引用该引用类? -

    public class Project
    {
         public int Id;
         public List<ProjectDependency> Dependencies;   //that is a cycle and it is very bad architecture
    }
    

    你到底为什么要这么做?是不是循环太多了?

    解决方案:(已编辑)

    我会建议像这样更简单的模型 -

    public class Project
    {
         public int Id;
         public List<ProjectDependency> Dependencies; 
    }
    
    public class ProjectDependency
    {
         //..... other property
         public Project DependentProject; 
    }
    

    就是这样。那是我的课。为什么?因为NHbernate会自动创建需要的外键。而且由于您使用的是 ORM,我认为 ORM 创建多少表和列来支持您并不重要,这是 ORM 的头痛而不是用户。但是,如果您愿意,您始终可以使用 ManyToMany 引用覆盖它,以便使用单独的表。

    现在,在修复类定义后,我会使用类似的方法来更新我的依赖项 -

    public class Project
    {
         public int Id;
         public List<ProjectDependency> Dependencies; 
    
         public List<ProjectDependency> AddDepedency(List<ProjectDependency> dependencies){
              dependencies.ForEach(d => {
                   if (Dependencies.All(x=> x.Id != d.Id)){
                        Dependencies.Add(d);
                   }
              });
              return Dependencies;
         }
    
         public List<ProjectDependency> RemoveDepedency(List<ProjectDependency> dependencies){
              dependencies.ForEach(d => {
                   if (Dependencies.Any(x=> x.Id == d.Id)){
                        Dependencies.Remove(d);
                   }
              });
              return Dependencies;
         }
    
         public List<ProjectDependency> UpdateDependency(List<ProjectDependency> dependencies){
              dependencies.ForEach(d => {
                   if (Dependencies.All(x=> x.Id != d.Id)){
                        Dependencies.Add(d);
                   }
              });
              Dependencies.RemoveAll(d => depdencies.All(x => x.Id != d.Id));
              return Depdendencies;
         }
    }
    

    注意事项 -

    1. 我发送List&lt;ProjectDependency&gt;而不是发送List&lt;int&gt;,这是因为Model不应该知道从系统中的数据库获取的服务,因此他们只会担心类。

    2. 无论当前列表中有什么,UpdateDependency 都会将其替换为提供的列表。所以迟早要从数据库中获取项目。因此,最好将它们预先设置并作为ProjectDependency 对象传递。这将保持架构整洁。

    3. 我添加了两个额外的方法,添加和删除,这是我实际的做法。但是由于您使用的是UpdateDependency,所以我也添加了一个。

    4. 另外请注意,我在每个函数中返回列表,这将有助于在与其他服务和类一起使用时嵌套方法。每次使用需要链接的方法时,它可以帮助您减少一行代码。

    最后但并非最不重要的一点是,我还没有测试过代码。因此,您可能会发现一些拼写或语法错误。如果您喜欢该代码,请更改或修复任何错误并使用它。

    【讨论】:

    • 感谢您的详细回复。就您的第一点而言,我在桥接表(ProjectDependency 表)中有其他字段,但我没有将它们包括在内以简化问题并且不会分散重点。如果这导致您花精力专注于这一点,我们深表歉意。鉴于这一点,请告诉我您的答案有何变化。
    • 是的,它让我改变了答案。但我通常更喜欢避免循环,我通常使用一种方式分配。因为除非你没有使用好的架构,否则在任何情况下都不需要调用 parent,这就是为什么它是子实体,而不是父实体。
    【解决方案3】:

    下面是一个控制台应用程序,它将定位未更改、添加和删除的 ProjectDependency Id。获取 Id 更容易,因为 List&lt;int&gt; 已传递给 UpdateDependencies 方法。希望这会有所帮助。

    class Program
    {
        static void Main(string[] args)
        {
            var project = CreateProject();
            var newDependencyIds = new List<int>() {2, 3, 4, 13};
            UpdateDependencies(project, newDependencyIds);
        }
    
        private static Project CreateProject()
        {
            var project = new Project() {Id = 1};
            project.Dependencies = new List<ProjectDependency>();
            for (int projectId = 2; projectId < 10; projectId++)
            {
                var dependency = new ProjectDependency() {Project = project, Dependency = new Project() {Id = projectId}};
                project.Dependencies.Add(dependency);
            }
            return project;
        }
    
        private static void UpdateDependencies(Project p, List<int> newDependencyIds)
        {
            var oldDependencyIds = p.Dependencies.Select(d => d.Dependency.Id);
            var unchanged = oldDependencyIds.Intersect(newDependencyIds);
            var added = newDependencyIds.Except(oldDependencyIds);
            var removed = oldDependencyIds.Except(newDependencyIds);
    
            Console.WriteLine("Old ProjectDependency Ids: " + string.Join(", ", oldDependencyIds));
            Console.WriteLine("New ProjectDependency Ids: " + string.Join(", ", newDependencyIds));
            Console.WriteLine();
    
            Console.WriteLine("Unchanged: " + string.Join(", ", unchanged));
            Console.WriteLine("Added: " + string.Join(", ", added));
            Console.WriteLine("Removed: " + string.Join(", ", removed));
            Console.WriteLine();
    
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }
    }
    

    输出是:

    Old ProjectDependency Ids: 2, 3, 4, 5, 6, 7, 8, 9
    New ProjectDependency Ids: 2, 3, 4, 13
    
    Unchanged: 2, 3, 4
    Added: 13
    Removed: 5, 6, 7, 8, 9
    
    Press any key to continue...
    

    【讨论】:

      【解决方案4】:

      请将此作为基本想法。

      在您的情况下,使用HashSet&lt;&gt; 进行实际收集和使用ICollection&lt;T&gt; 进行声明会更好。从数据库中检索时,NHibernate 将使用 ISet&lt;&gt; 用于版本 = 4.0(当前处于 alpha 阶段)。

      然后在您的ProjectDependency 中,您应该实现GetHashCode 和Equals 覆盖以正确识别集合中是否已经存在相同的项目。

      有了这个,Contains 和所有Linq 操作应该能够检测到重复。

      public class Project
      {
          public virtual int Id { get; set; }
          public virtual ICollection<ProjectDependency> Dependencies { get; set; }
      
          public Project()
          {
              this.Dependencies = new HashSet<ProjectDependency>();
          }
      }
      
      public class ProjectDependency
      {
          public Project Project;
          public Project Dependency;
      
          // This is a simplified version and don't check for nulls in internal members
          // or transient objects
          public override int GetHashCode()
          {
              return this.Project.Id + this.Dependency.Id;
          }
      
          // This is a simplified version and don't check for nulls in internal members
          // or transient objects
          public override bool Equals(object obj)
          {
              var dep = obj as ProjectDependency;
              if (dep == null)
              {
                  return false;
              }
      
              return this.Project.Id == dep.Project.Id && this.Dependency.Id == dep.Dependency.Id;
          }
      }
      

      现在,你的更新方法可以简化为:

      public void UpdateDependencies(Project p, List<int> newDependencyIds)
      {
          var newDependencies = newDependencyIds.Select(d => new ProjectDependency{ Project = p, Dependency = GetDependency(d) });
      
          var addDependencies = newDependencies.Except(p.Dependencies);
          var delDependencies = p.Dependencies.Except(newDependencies);
      
          foreach (var dependency in addDependencies)
          {
              p.Dependencies.Add(dependency);
          }
      
          foreach (var dependency in delDependencies)
          {
              p.Dependencies.Remove(dependency);
          }
      }
      

      现在它只会更新修改过的项目。

      更新:在此答案中添加了来自 @doan-van-tuan 的建议。

      更新 2:如果您的 ProjectDependency 类没有任何其他属性并且仅用作多对多类,请考虑以下事项。

      在这种情况下,您可以删除ProjectDependency 类,如下所示:

      public class Project
      {
          // This attribute is used to ensure GetHashCode always return the same value
          private int? hashCode;
      
          public virtual int Id { get; set; }
          public virtual ICollection<Project> Dependencies { get; set; }
      
          public Project()
          {
              this.Dependencies = new HashSet<Project>();
          }
      
          // This is a simplified but correct implementation of GetHashCode
          public override int GetHashCode()
          {
              if (this.hashCode.HasValue)
              {
                  return this.hashCode.Value;
              }
      
              if (this.Id == 0)
              {
                  return (this.hashCode = base.GetHashCode()).Value;
              }
      
              return (this.hashCode = typeof(Project).GetHashCode() * this.Id * 251).Value;
          }
      
          public override bool Equals(object obj)
          {
              var p = obj as Project;
              if (Object.ReferenceEquals(p, null))
              {
                  return false;
              }
      
              return p.Id == this.Id;
          }
      }
      
      public class ProjectMapping : ClassMapping<Project>
      {
          public ProjectMapping()
          {
              this.Table("Project");
      
              this.Id(x => x.Id, mapper => mapper.Generator(Generators.Assigned));
              this.Set(x => x.Dependencies,
                  mapper =>
                  {
                      mapper.Table("ProjectDependency");
                      mapper.Key(m => m.Column("ProjectId"));
                  },
                  mapper =>
                  {
                      mapper.ManyToMany(m => m.Column("DependencyId"));
                  });
          }
      }
      

      您的更新方法可能是:

      public void UpdateDependencies(Project p, IEnumerable<Project> newDependencies)
      {
          var addDependencies = newDependencies.Except(p.Dependencies);
          var delDependencies = p.Dependencies.Except(newDependencies);
      
          foreach (var dependency in addDependencies)
          {
              p.Dependencies.Add(dependency);
          }
      
          foreach (var dependency in delDependencies)
          {
              p.Dependencies.Remove(dependency);
          }
      }
      

      【讨论】:

      • 4 是什么意思。看起来 3.3.3 是最新版本:nhforge.org
      • NHibernate 4 仍处于 alpha 阶段,主要变化是它将取代 Iesi.Collections 库,以支持原生 .net 集合。
      • 我从 NH2.2 开始就使用 Set 来应对这些情况。这个答案是正确的,应该奖励赏金。对答案的小补充:您不必使用 ISet。请改用 ICollection 和 HashSet。
      • 编辑了答案,将 ISet 替换为 ICollection.. 对于 GetHashCode,如果 ProjectDependency 有代理键,最好使用该键作为哈希码,而不是自然键对(项目、依赖项)
      • @doan-van-tuan,有人撤消了您的编辑。我同意你的两点。 ICollection 的使用和虚拟的使用(我的错误在这里)。关于代理键,我不能同意。作为同一个项目,依赖对可以有不同的代理键(当然你可以在数据库上使用唯一索引来避免它)。在这种情况下,哈希应该反映对象的内容,因此它必须是密钥对。
      【解决方案5】:

      您是如何得出项目具有依赖关系的?你只声明你有一个表,所以我假设实际上没有一个名为 ProjectDependency 的表。

      但是,如果有这样的表,它可能是多余的。如果只有 Project 表,那就更简单了。忍受我:

      如果是我并且我能够修改表,我会让 Project 有一个自引用(递归)外键。这样该项目表将有一个名为 ParentProjectId 的列,其类型与表项目的主键相同,然后将主键作为外键约束引用。

      类似这样的东西(这是用于 ms-sql 服务器的,但这个概念应该存在于其他数据库中,如果不让我知道的话):

      CREATE TABLE [dbo].[Project](
          [Id] [int] IDENTITY(1,1) NOT NULL,
          [Name] [nvarchar](50) NULL,
          [ParentProjectId] [int] NULL,
       CONSTRAINT [PK_Project] PRIMARY KEY CLUSTERED 
      (
          [Id] ASC
      )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
      ) ON [PRIMARY]
      
      GO
      
      ALTER TABLE [dbo].[Project]  WITH CHECK ADD  CONSTRAINT [FK_ParentProject_Project] FOREIGN KEY([ParentProjectId])
      REFERENCES [dbo].[Project] ([Id])
      GO
      
      ALTER TABLE [dbo].[Project] CHECK CONSTRAINT [FK_ParentProject_Project]
      GO
      

      如果可能的话,我会按照以下方式建模:

      public class Project
      {
          private IList<Project> _dependencies;
      
          public virtual int? Id { get; set; }
      
          public virtual string Name { get; set; }
      
          public virtual IList<Project> Dependencies
          {
              get { return _dependencies ?? (_dependencies = new List<Project>()); }
              set { _dependencies = value; }
          }
      
          public virtual Project ParentProject { get; set; }
      
          public virtual Project AddDependency(Project dependency)
          {
              dependency.ParentProject = this;
              Dependencies.Add(dependency);
      
              return this;
          }
      }
      

      然后在映射中我会这样映射(暂时是伪代码,将更新):

      public sealed class ProjectMap : ClassMap<Project>
      {
          public ProjectMap()
          {
              Id(x => x.Id).Column("Id").GeneratedBy.Native();
              Map(x => x.Name);
      
              References(x => x.ParentProject).Column("ParentProjectId").Cascade.All();
      
              HasMany(x => x.Dependencies).KeyColumn("ParentProjectId").Inverse().Cascade.AllDeleteOrphan();
          }
      }
      

      这基本上使您能够使用递归无限地嵌套项目。

      现在,根据您的问题,您正在使用 nhibernate,并且您的项目实体具有 int 类型的主键。通常我也会将该键设为标识列,并且我上面的映射让 nhibernate 知道数据库将生成它。

      仅基于您询问“最优雅的方式”是否是我的帖子,基于您的模型,并假设我有能力/权限使用数据库架构,这就是我的做法。

      AddDependency 方法的工作方式是它处理将依赖项连接到项目,当您实际对项目实体执行 Session.Save 时,NHibernate 实际上只会插入那些新项目。我会简单地创建或选择新项目并将它们传递进来,而不是尝试使用 id,然后让数据库处理它(因为它擅长它)

      我可能不会尝试做多对多,但如果这是你真正需要的,请告诉我,我会更新我的答案。我只是说你有'我的 SQL 服务器数据库中有一个表'

      第

      【讨论】:

      • 它是多对多的关系,所以一个项目不应该被限制为只有一个“父”
      猜你喜欢
      • 1970-01-01
      • 2021-10-27
      • 1970-01-01
      • 1970-01-01
      • 2012-05-08
      • 1970-01-01
      • 2017-01-22
      • 2010-12-15
      相关资源
      最近更新 更多