使用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 缓存数据,而不是对象实例。

当然,覆盖相等性/哈希码在这里没有帮助,因为导致问题的不是相等性。事实上,我在这里有两个相同的实例。

这都很好,但是如何处理呢?有多种选择,但似乎都不太吸引我:

  1. 引入一个中间服务层,它将实例缓存在哈希表中,并在从存储库加载实例后遍历实体图。我不太喜欢它,因为它需要大量工作,容易出错,而且听起来像是我在做 ORM 的工作。如果我想这样做,我宁愿手动实现整个持久性,但我没有。

  2. 引入单个聚合根并将其从数据库中拉出一次,而不是获取多个部分。它可以工作,因为我的应用程序相对简单,我可以处理整个图形。事实上,无论如何我都在使用它。但我不喜欢这样,因为它引入了不必要的实体。工作应该是工作,员工应该是员工。当然,我可以将神实体命名为“组织”之类的。我不喜欢它的另一个原因是,如果将来数据增长,它会变得笨拙。例如,我可能希望存档旧的工作和任务。

  3. 对所有内容使用单个会话。现在,我正在打开和关闭会话,因为我需要加载/存储一些东西。我可以使用单个会话,并且 NHibernate 的身份映射将保证引用身份(只要我不使用延迟加载)。这似乎是最好的选择,但应用程序可能 运行 一段时间(它是 WPF 桌面应用程序),我不喜欢让会话打开太久的想法。

  4. 手动更新所有实例。例如,如果我想更改职位描述,我会调用一些服务方法来搜索具有相同 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 工作单元模式来解决这个问题。使用命令模式还可以让我很好地撤消更改。但我还不愿意走那么远。