ORM和对象标识存在一个众所周知的问题。就ORM而言,如果实体具有相同的ID,则它们是相等的。当然,这并不适用于被认为是不存在的瞬态实例。
但就OO代码而言,如果对象引用引用相同的实例,则它们被认为是相等的。也就是说,除非Equals
和/或==
被覆盖。
这一切都很好,但在实践中意味着什么?这是一个非常简单的示例域模型:
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);
}
当然,它打印出“非常重要的工作”。启用二级缓存(已注释掉)不会更改它,尽管在某些情况下它会减少数据库命中。显然这是因为NHibernate缓存数据,而不是对象实例。
当然,覆盖相等/哈希码在这里没有帮助,因为它不是导致问题的相等性。事实上,我在这里有两个相同的实例。
这一切都很好,但如何处理呢?有几种选择,但对我来说似乎都没有吸引力:
引入一个中间服务层,它将在哈希表中缓存实例,并在从存储库加载实体图之后遍历实体图。我不是很喜欢它,因为它涉及很多工作,容易出错和听起来像我在做ORM的工作。如果我想这样做,我宁愿手动实现整个持久性,但我不是。
引入单个聚合根并从DB中提取一次,而不是获取多个部分。它可以工作,因为我的应用程序相对简单,我可以处理整个图形。事实上,无论如何我都在使用它。但我不喜欢这样,因为它引入了不必要的实体。乔布斯应该是工作,员工应该是员工。当然,我可以将上帝实体命名为“组织”或其他东西。我不喜欢它的另一个原因是,如果将来数据增长,它会变得笨拙。例如,我可能希望存档旧的工作和任务。
对所有内容使用单个会话。现在,我打开和关闭会话,因为我需要加载/存储一些东西。我可以使用单个会话,NHibernate的身份地图将保证参考身份(只要我不使用延迟加载)。这似乎是最好的选择,但应用程序可能会运行一段时间(它是一个WPF桌面应用程序),我不喜欢让会话打开太久的想法。
手动更新所有实例。例如,如果我想更改作业描述,我会调用一些服务方法来搜索具有相同ID的作业实例并将其全部更新。这可能会变得非常混乱,因为该服务必须能够访问基本上所有内容,基本上成为一种上帝服务。
我错过了其他任何选项?令我惊讶的是,关于这个问题的信息很少。我能找到的最好的是this post,但它只是处理平等问题。
答案 0 :(得分:0)
好的,这就是我想出来的。基本上,这是选项3的一些调整。我的repo类现在看起来像这样:
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()
看起来像是对我的黑客攻击。但其他选择看起来并不吸引人。
Callum建议在一个会话中加载所有内容然后稍后在另一个会话中保存要好得多,但它不适合我的特定应用程序。要弄清楚变化是什么,这将太复杂了。我可以通过使用Command和/或Unit of Work模式解决这个问题。使用Command模式也可以很好地撤消更改。但我还不愿意走那么远。