Каков самый элегантный способ обновления дочерней коллекции при использовании nhibernate (без создания ненужных добавлений и удалений)?

У меня есть объект домена, называемый Project, который сопоставляется с таблицей в моей базе данных SQL-сервера. Он имеет свойство, которое является списком, называемым зависимостями.

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

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

и я пытаюсь выяснить наиболее эффективный способ обновления списка зависимостей, учитывая новый список зависимостей.

Итак, вот наивная реализация:

 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 (что было добавлено, что было удалено) и просто внести эти изменения, так что если зависимость была там до и после этого, она не трогается.

Ответ 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)}));
    }

Ответ 2

Я действительно запутался в ваших классах Model. Вы создаете класс Project.

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

который сам передается в другом классе -

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

и снова вы ссылаетесь на этот класс referrer внутри указанного класса? -

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

Почему бы вам это сделать? Разве это не слишком много циклов?

РЕШЕНИЕ: (EDITED)

Я бы предложил более простую модель -

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

Заметки -

  • Вместо отправки List<int> я отправляю List<ProjectDependency>, это потому, что Model не должен знать о сервисах для извлечения из базы данных в системе, и поэтому они будут беспокоиться только о классах.

  • Независимо от того, что в текущем списке, UpdateDependency заменит его предоставленным списком. Поэтому рано или поздно предметы будут доставлены из db. Так что лучше, если они предварительно заполнены и переданы как объект ProjectDependency. Это позволит сохранить архитектуру чистой.

  • Я добавил два дополнительных метода: "Добавить и удалить", это фактический способ, которым я бы это сделал. Но поскольку вы используете UpdateDependency, я добавил это тоже.

  • Также обратите внимание, что я возвращаю список в каждой функции, это поможет при вложении методов при использовании с другими службами и классами. Это помогает вам сократить одну строку кода каждый раз, когда вы используете те методы, которые нуждаются в цепочке.

Наконец, но не менее того, я не тестировал код. Таким образом, вы можете найти некоторую орфографическую или синтаксическую ошибку. Если вам нравится код, измените или исправьте ошибки, если они есть, и используйте его.

Ответ 3

Пожалуйста, возьмите это как базовую идею.

В вашем случае лучше работать с HashSet<> для фактической коллекции и ICollection<T> для объявления. При извлечении из базы данных NHibernate будет использовать ISet<> для версии <= 3.3 и HashSet для >= 4.0 (в настоящее время в альфа).

Затем в вашем 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

Ниже приведено консольное приложение, в котором будут найдены неизменные, добавленные и удаленные идентификаторы ProjectDependency. Легче получить идентификаторы, потому что 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...

Ответ 5

как вы определяете, что у проекта есть зависимости? вы утверждаете, что у вас есть одна таблица, поэтому я предполагаю, что на самом деле нет таблицы с именем ProjectDependency.

однако, если есть такая таблица, она может быть избыточной. если есть только таблица Project, то это еще проще. медведь со мной:

если бы это был я, и если бы мне удалось изменить таблицу, я бы сделал проект имеющим собственный рекурсивный внешний ключ. так что эта таблица проекта будет иметь столбец с именем ParentProjectId того же типа, что и первичный ключ проекта таблицы, который затем ссылается на первичный ключ как ограничение внешнего ключа.

что-то вроде этого (это для сервера ms-sql, но концепция должна стоять в других db, если не дайте мне знать):

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.Save на объекте проекта, NHibernate фактически будет только вставлять эти новые проекты. и я просто создавал или выбирал новые проекты и передавал их, вместо того, чтобы пытаться работать с идентификаторами, и просто позволял базе данных обрабатывать это (потому что это хорошо)

Я бы, вероятно, не пытался и сделал много для многих, но если это то, что вам действительно нужно, дайте мне знать, и я обновлю свой ответ. Я просто убираю то, что вы сказали, что у вас есть "таблица в моей базе данных SQL Server"

HTH