使用NHibernate时如何处理不同的实体实例?
How to deal with different entity instances when using NHibernate?
ORM 和对象标识存在一个众所周知的问题。就 ORM 而言,如果实体具有相同的 ID,则它们是平等的。当然,这不适用于被认为不存在的瞬态实例。
但就面向对象代码而言,如果对象引用引用同一实例,则对象引用被认为是相等的。也就是说,除非 Equals
and/or ==
被覆盖。
这很好,但这在实践中意味着什么?这是一个非常简单的域模型示例:
namespace TryHibernate.Example
{
public abstract class Entity
{
public int Id { get; set; }
}
public class Employee : Entity
{
public string Name { get; set; }
public IList<Task> Tasks { get; set; }
public Employee()
{
Tasks = new List<Task>();
}
}
public class Task : Entity
{
public Employee Assignee { get; set; }
public Job Job { get; set; }
}
public class Job : Entity
{
public string Description { get; set; }
}
} // namespace
下面是使用它的示例代码:
using (ISessionFactory sessionFactory = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.UsingFile("temp.sqlite").ShowSql())
//.Cache(c => c.UseSecondLevelCache().UseQueryCache().ProviderClass<HashtableCacheProvider>())
.Mappings(m => m.AutoMappings.Add(
AutoMap.AssemblyOf<Entity>()
.Where(type => type.Namespace == typeof(Entity).Namespace)
.Conventions.Add(DefaultLazy.Never())
.Conventions.Add(DefaultCascade.None())
//.Conventions.Add(ConventionBuilder.Class.Always(c => c.Cache.ReadWrite()))
).ExportTo("hbm")
).ExposeConfiguration(c => new SchemaExport(c).Create(true, true))
.BuildSessionFactory())
{
Job job = new Job() { Description = "A very important job" };
Employee empl = new Employee() { Name = "John Smith" };
Task task = new Task() { Job = job, Assignee = empl };
using (ISession db = sessionFactory.OpenSession())
using (ITransaction t = db.BeginTransaction())
{
db.Save(job);
db.Save(empl);
empl.Tasks.Add(task);
db.Save(task);
t.Commit();
}
IList<Job> jobs;
using (ISession db = sessionFactory.OpenSession())
{
jobs = db.QueryOver<Job>().List();
}
IList<Employee> employees;
using (ISession db = sessionFactory.OpenSession())
{
employees = db.QueryOver<Employee>().List();
}
jobs[0].Description = "A totally unimportant job";
Console.WriteLine(employees[0].Tasks[0].Job.Description);
}
当然会打印“A very important job”。启用二级缓存(注释掉)不会改变它,尽管它在某些情况下会减少数据库命中率。显然那是因为 NHibernate 缓存数据,而不是对象实例。
当然,覆盖相等性/哈希码在这里没有帮助,因为导致问题的不是相等性。事实上,我在这里有两个相同的实例。
这都很好,但是如何处理呢?有多种选择,但似乎都不太吸引我:
引入一个中间服务层,它将实例缓存在哈希表中,并在从存储库加载实例后遍历实体图。我不太喜欢它,因为它需要大量工作,容易出错,而且听起来像是我在做 ORM 的工作。如果我想这样做,我宁愿手动实现整个持久性,但我没有。
引入单个聚合根并将其从数据库中拉出一次,而不是获取多个部分。它可以工作,因为我的应用程序相对简单,我可以处理整个图形。事实上,无论如何我都在使用它。但我不喜欢这样,因为它引入了不必要的实体。工作应该是工作,员工应该是员工。当然,我可以将神实体命名为“组织”之类的。我不喜欢它的另一个原因是,如果将来数据增长,它会变得笨拙。例如,我可能希望存档旧的工作和任务。
对所有内容使用单个会话。现在,我正在打开和关闭会话,因为我需要加载/存储一些东西。我可以使用单个会话,并且 NHibernate 的身份映射将保证引用身份(只要我不使用延迟加载)。这似乎是最好的选择,但应用程序可能 运行 一段时间(它是 WPF 桌面应用程序),我不喜欢让会话打开太久的想法。
手动更新所有实例。例如,如果我想更改职位描述,我会调用一些服务方法来搜索具有相同 ID 的职位实例并更新它们。这可能会变得非常混乱,因为该服务必须能够访问基本上所有的东西,本质上成为一种上帝服务。
还有其他我错过的选择吗?令我惊讶的是关于这个问题的信息很少。我能找到的最好的是 this post,但它只是处理平等问题。
好的,这就是我的想法。基本上,这是经过一些调整的选项 3。我的回购 class 现在看起来像这样:
public class Repo : IDisposable
{
private ISessionFactory sessionFactory;
private ISession session;
public Repo(ISessionFactory sessionFactory)
{
this.sessionFactory = sessionFactory;
this.session = sessionFactory.OpenSession();
session.Disconnect();
}
public void Dispose()
{
try
{
session.Dispose();
}
finally
{
sessionFactory.Dispose();
}
}
public void Save(Entity entity)
{
using (Connection connection = new Connection(session))
using (ITransaction t = session.BeginTransaction())
{
session.Save(entity);
t.Commit();
}
}
public IList<T> GetList<T>() where T : Entity
{
using (Connection connection = new Connection(session))
{
return session.QueryOver<T>().List();
}
}
private class Connection : IDisposable
{
private ISession session;
internal Connection(ISession session)
{
this.session = session;
session.Reconnect();
}
public void Dispose()
{
session.Disconnect();
}
}
}
它的用法是这样的:
using (Repo db = new Repo(sessionFactory))
{
Job job = new Job() { Description = "A very important job" };
Employee empl = new Employee() { Name = "John Smith" };
Task task = new Task() { Job = job, Assignee = empl };
db.Save(job);
db.Save(empl);
empl.Tasks.Add(task);
db.Save(task);
IList<Job> jobs;
jobs = db.GetList<Job>();
IList<Employee> employees;
employees = db.GetList<Employee>();
jobs[0].Description = "A totally unimportant job";
Console.WriteLine(employees[0].Tasks[0].Job.Description);
}
别管缺少交易,当然在现实生活中比这复杂一点,我只是省略了不相关的部分。重要的是它可以工作,并且不会无限期地保持打开的连接。
不过我还是不喜欢这个。看起来我在这里滥用了 ORM,方法 Disconnect()
和 Reconnect()
对我来说就像是一个 hack。但其他选项看起来一点吸引力都没有。
Callum 的建议是在一个会话中加载所有内容,然后在另一个会话中保存,这要好得多,但它不适合我的特定应用程序。要弄清楚发生了什么变化太复杂了。我可以通过使用命令 and/or 工作单元模式来解决这个问题。使用命令模式还可以让我很好地撤消更改。但我还不愿意走那么远。
ORM 和对象标识存在一个众所周知的问题。就 ORM 而言,如果实体具有相同的 ID,则它们是平等的。当然,这不适用于被认为不存在的瞬态实例。
但就面向对象代码而言,如果对象引用引用同一实例,则对象引用被认为是相等的。也就是说,除非 Equals
and/or ==
被覆盖。
这很好,但这在实践中意味着什么?这是一个非常简单的域模型示例:
namespace TryHibernate.Example
{
public abstract class Entity
{
public int Id { get; set; }
}
public class Employee : Entity
{
public string Name { get; set; }
public IList<Task> Tasks { get; set; }
public Employee()
{
Tasks = new List<Task>();
}
}
public class Task : Entity
{
public Employee Assignee { get; set; }
public Job Job { get; set; }
}
public class Job : Entity
{
public string Description { get; set; }
}
} // namespace
下面是使用它的示例代码:
using (ISessionFactory sessionFactory = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.UsingFile("temp.sqlite").ShowSql())
//.Cache(c => c.UseSecondLevelCache().UseQueryCache().ProviderClass<HashtableCacheProvider>())
.Mappings(m => m.AutoMappings.Add(
AutoMap.AssemblyOf<Entity>()
.Where(type => type.Namespace == typeof(Entity).Namespace)
.Conventions.Add(DefaultLazy.Never())
.Conventions.Add(DefaultCascade.None())
//.Conventions.Add(ConventionBuilder.Class.Always(c => c.Cache.ReadWrite()))
).ExportTo("hbm")
).ExposeConfiguration(c => new SchemaExport(c).Create(true, true))
.BuildSessionFactory())
{
Job job = new Job() { Description = "A very important job" };
Employee empl = new Employee() { Name = "John Smith" };
Task task = new Task() { Job = job, Assignee = empl };
using (ISession db = sessionFactory.OpenSession())
using (ITransaction t = db.BeginTransaction())
{
db.Save(job);
db.Save(empl);
empl.Tasks.Add(task);
db.Save(task);
t.Commit();
}
IList<Job> jobs;
using (ISession db = sessionFactory.OpenSession())
{
jobs = db.QueryOver<Job>().List();
}
IList<Employee> employees;
using (ISession db = sessionFactory.OpenSession())
{
employees = db.QueryOver<Employee>().List();
}
jobs[0].Description = "A totally unimportant job";
Console.WriteLine(employees[0].Tasks[0].Job.Description);
}
当然会打印“A very important job”。启用二级缓存(注释掉)不会改变它,尽管它在某些情况下会减少数据库命中率。显然那是因为 NHibernate 缓存数据,而不是对象实例。
当然,覆盖相等性/哈希码在这里没有帮助,因为导致问题的不是相等性。事实上,我在这里有两个相同的实例。
这都很好,但是如何处理呢?有多种选择,但似乎都不太吸引我:
引入一个中间服务层,它将实例缓存在哈希表中,并在从存储库加载实例后遍历实体图。我不太喜欢它,因为它需要大量工作,容易出错,而且听起来像是我在做 ORM 的工作。如果我想这样做,我宁愿手动实现整个持久性,但我没有。
引入单个聚合根并将其从数据库中拉出一次,而不是获取多个部分。它可以工作,因为我的应用程序相对简单,我可以处理整个图形。事实上,无论如何我都在使用它。但我不喜欢这样,因为它引入了不必要的实体。工作应该是工作,员工应该是员工。当然,我可以将神实体命名为“组织”之类的。我不喜欢它的另一个原因是,如果将来数据增长,它会变得笨拙。例如,我可能希望存档旧的工作和任务。
对所有内容使用单个会话。现在,我正在打开和关闭会话,因为我需要加载/存储一些东西。我可以使用单个会话,并且 NHibernate 的身份映射将保证引用身份(只要我不使用延迟加载)。这似乎是最好的选择,但应用程序可能 运行 一段时间(它是 WPF 桌面应用程序),我不喜欢让会话打开太久的想法。
手动更新所有实例。例如,如果我想更改职位描述,我会调用一些服务方法来搜索具有相同 ID 的职位实例并更新它们。这可能会变得非常混乱,因为该服务必须能够访问基本上所有的东西,本质上成为一种上帝服务。
还有其他我错过的选择吗?令我惊讶的是关于这个问题的信息很少。我能找到的最好的是 this post,但它只是处理平等问题。
好的,这就是我的想法。基本上,这是经过一些调整的选项 3。我的回购 class 现在看起来像这样:
public class Repo : IDisposable
{
private ISessionFactory sessionFactory;
private ISession session;
public Repo(ISessionFactory sessionFactory)
{
this.sessionFactory = sessionFactory;
this.session = sessionFactory.OpenSession();
session.Disconnect();
}
public void Dispose()
{
try
{
session.Dispose();
}
finally
{
sessionFactory.Dispose();
}
}
public void Save(Entity entity)
{
using (Connection connection = new Connection(session))
using (ITransaction t = session.BeginTransaction())
{
session.Save(entity);
t.Commit();
}
}
public IList<T> GetList<T>() where T : Entity
{
using (Connection connection = new Connection(session))
{
return session.QueryOver<T>().List();
}
}
private class Connection : IDisposable
{
private ISession session;
internal Connection(ISession session)
{
this.session = session;
session.Reconnect();
}
public void Dispose()
{
session.Disconnect();
}
}
}
它的用法是这样的:
using (Repo db = new Repo(sessionFactory))
{
Job job = new Job() { Description = "A very important job" };
Employee empl = new Employee() { Name = "John Smith" };
Task task = new Task() { Job = job, Assignee = empl };
db.Save(job);
db.Save(empl);
empl.Tasks.Add(task);
db.Save(task);
IList<Job> jobs;
jobs = db.GetList<Job>();
IList<Employee> employees;
employees = db.GetList<Employee>();
jobs[0].Description = "A totally unimportant job";
Console.WriteLine(employees[0].Tasks[0].Job.Description);
}
别管缺少交易,当然在现实生活中比这复杂一点,我只是省略了不相关的部分。重要的是它可以工作,并且不会无限期地保持打开的连接。
不过我还是不喜欢这个。看起来我在这里滥用了 ORM,方法 Disconnect()
和 Reconnect()
对我来说就像是一个 hack。但其他选项看起来一点吸引力都没有。
Callum 的建议是在一个会话中加载所有内容,然后在另一个会话中保存,这要好得多,但它不适合我的特定应用程序。要弄清楚发生了什么变化太复杂了。我可以通过使用命令 and/or 工作单元模式来解决这个问题。使用命令模式还可以让我很好地撤消更改。但我还不愿意走那么远。