Каков самый элегантный способ обновления дочерней коллекции при использовании 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