在使用nhibernate时,更新子集合最优雅的方式是什么(不创建不必要的添加和删除)?

本文关键字:创建 是什么 不必要 添加 删除 方式 nhibernate 更新 子集合 | 更新日期: 2023-09-27 18:09:21

我有一个名为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});
       }
 }

但是这里的问题是,即使没有任何更改,我也会清除所有项并在之前存在的相同项上进行插入。

我正在寻找一种优雅的方式来确定diff(添加了什么,删除了什么),只是做出这些改变,所以如果一个依赖关系之前和之后,那么它不会被触及。

在使用nhibernate时,更新子集合最优雅的方式是什么(不创建不必要的添加和删除)?

    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)}));
    }

我对你的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<int>,而是发送List<ProjectDependency>,这是因为Model不应该知道系统中从数据库中获取的服务,因此他们只会担心类。

  2. 无论当前列表中有什么,UpdateDependency都会将其替换为提供的列表。所以这些项迟早会从db中获取。因此,最好将它们预先设置为ProjectDependency对象。

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

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

最后,但并非最不重要的是,我没有测试代码。所以你可能会发现一些拼写或语法错误。如果您喜欢代码,那么请更改或修复错误(如果有的话)并使用它。

下面是一个控制台应用程序,它将定位未更改,添加和删除的ProjectDependency id。获取id更容易,因为List<int>被传递给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...

请以此为基本思路。

在您的情况下,最好使用HashSet<>进行实际收集,ICollection<T>进行声明。当从数据库中检索时,NHibernate将使用ISet<>版本<= 3.3和HashSet版本>= 4.0(目前在alpha中)。

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

这样,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对这个答案的建议。

UPDATE 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);
    }
}

你是如何得出项目有依赖关系的?您只声明您有一个表,所以我假设实际上没有名为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,并且您的Project实体有一个int类型的主键。通常我会让这个键也成为一个标识列,我上面的映射让nhibernate知道数据库将生成它。

只是基于你的帖子问"最优雅的方式",如果是我,基于你的模型,假设我有能力/权限使用数据库模式,这就是我将如何做到的。

AddDependency方法的工作方式是,它处理将依赖项连接到项目,以及当您实际执行Session时。保存在项目实体上,NHibernate实际上只会插入那些新项目。我会简单地创建或选择新的项目,并传递给他们,而不是试图与id的工作,只是让数据库处理(因为它很擅长)

我可能不会尝试做多对多,但如果这是你真正需要让我知道,我会更新我的答案。我只是按照你说的在我的SQL server数据库中有一个表

hth