级联保存 - StaleObjectStateException:行已被另一个事务更新或删除

Cascade Save - StaleObjectStateException: Row was updated or deleted by another transaction

更新 NHibernate 版本时遇到问题。当前版本是 3.3.1.4000,正在尝试更新到 4。 更新单元测试后,用级联保存失败:

NHibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [NHibernateTests.TestMappings.ProductLine#cdcaf08d-4831-4882-84b8-14de91581d2e]

映射:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernateTests" namespace="NHibernateTests.TestMappings">
    <class name="Product" lazy="false" table="UserTest">
        <id name="Id">
            <generator class="guid"></generator>
        </id>

        <version name="Version" column="Version" unsaved-value="0"/>
        <property name="Name" not-null="false"></property>
        <property name="IsDeleted"></property>

        <bag name="ProductLines" table="ProductLine" inverse="true" cascade="all" lazy="true" where="IsDeleted=0" >
            <cache usage="nonstrict-read-write" />
            <key column="UserId" />
            <one-to-many class="ProductLine"  />
        </bag>
    </class>
</hibernate-mapping>


<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernateTests" namespace="NHibernateTests.TestMappings">
    <class name="ProductLine" where="IsDeleted=0" lazy="false">
        <cache usage="nonstrict-read-write" />
        <id name="Id">
            <generator class="guid"></generator>
        </id>

        <version name="Version" column="Version" unsaved-value="0"/>
        <property name="IsDeleted"></property>

        <many-to-one name="Product" class="Product" column="UserId" not-null="true" lazy="proxy"></many-to-one>

    </class>
</hibernate-mapping>

类:

public class Product
{
    public Guid Id { get; set; }
    public int Version { get; set; }
    public bool IsDeleted { get; set; }

    public string Name { get; set; }
    public bool IsActive { get; set; }
    public IList<ProductLine> ProductLines { get; private set; }

    public Product()
    {
        ProductLines = new List<ProductLine>();
    }
}

public class ProductLine
{
    public Guid Id { get; set; }
    public int Version { get; set; }
    public bool IsDeleted { get; set; }

    public Product Product { get; set; }
}

测试:

[TestMethod]
public void CascadeSaveTest()
{
    var product = new Product
    {
        Id = Guid.NewGuid(),
        Name = "aaa",
        IsActive = true
    };
    var productLine = new ProductLine
    {
        Id = Guid.NewGuid(),
        Product = product,
    };
    product.ProductLines.Add(productLine);

    using (var connection = new RepositoryConnection())
    {
        using (var repositories = new Repository<Product>(connection))
        {
            repositories.Create(product);
            //the below just calls the Session.Transaction.Commit();
            connection.Commit();   //NH3.3.1.400 passes, NH4 fails
        }
    }
}

提前感谢您的想法。

好吧,我想我现在已经明白是什么导致了 NH 4 的错误。如果我是对的,那是一个有点人为的案例,导致这种行为很难被认定为错误。

在您的示例中,产品系列通过 bag 映射。 bag 可以包含重复项,这需要 ProductProductLine 之间的中间值 table。 (类似于带有 UserId (ProductId) 列和 ProductLineId 列的 ProductProductLine table。)

您已将此中间 table 设置为 ProductLine table。我怀疑对 db 的提交导致 NHibernate 插入产品,然后是产品线,然后尝试通过在 ProductLine table 中再次插入来插入关系。 (您可以通过在您的数据库上分析 SQL 查询来检查这一点。)

事情有点混乱,因为 doc 状态(强调是我的):

table (optional - defaults to property name) the name of the collection table (not used for one-to-many associations)

但是,如何遵守 collection 中允许重复的 bag 语义?来自同一文档:

A bag is an unordered, unindexed collection which may contain the same element multiple times.

无论如何,在您的示例中,您确实应该将 one-to-many 映射到 set,如 doc.

所示
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernateTests" namespace="NHibernateTests.TestMappings">
    <class name="Product" lazy="false" table="UserTest">
        <id name="Id">
            <generator class="guid"></generator>
        </id>

        <version name="Version" column="Version" unsaved-value="0"/>
        <property name="Name" not-null="false"></property>
        <property name="IsDeleted"></property>

        <set name="ProductLines" inverse="true" cascade="all" lazy="true" where="IsDeleted=0" >
            <cache usage="nonstrict-read-write" />
            <key column="UserId" />
            <one-to-many class="ProductLine"  />
        </set>
    </class>
</hibernate-mapping>

并更改您的 collection 类型以使用 .Net fx 4 System.Collections.Generic.ISet<T>

public ISet<ProductLine> ProductLines { get; private set; }

public Product()
{
    ProductLines = new HashSet<ProductLine>();
}

如果这导致您的问题消失,则意味着 NH 4 中 bag 处理方式发生了一些变化。但是我们是否应该将此变化视为一个错误?不确定,因为在这种情况下使用 bag 不适合我。

进一步深入研究发现 NHibernate4 在与 Cascade 相关时无法识别它是新实体还是已经存在的实体。 对于有问题的场景,它正在为 ProductLine 调用 SQL Update,而不是 Create

它适用于以下更改,但我对 NHibernate 版本之间的此类更改感到非常困惑。

更改为 ProductLine 映射

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernateTests" namespace="NHibernateTests.TestMappings">
    <class name="ProductLine" where="IsDeleted=0" lazy="false">
        <cache usage="nonstrict-read-write" />
        <!-- here comes the updated line -->
        <id name="Id" type="guid" unsaved-value="00000000-0000-0000-0000-000000000000">
            <generator class="guid"></generator>
        </id>

        <version name="Version" column="Version" unsaved-value="0"/>
        <property name="IsDeleted"></property>

        <many-to-one name="Product" class="Product" column="UserId" not-null="true" lazy="proxy"></many-to-one>

    </class>
</hibernate-mapping>

更改测试方法

[TestMethod]
public void CascadeSaveTest()
{
    var product = new Product
    {
        Id = Guid.NewGuid(),
        Name = "aaa",
        IsActive = true
    };
    var productLine = new ProductLine
    {
        Id = Guid.Empty,            //The updated line
        Product = product,
    };
    product.ProductLines.Add(productLine);

    using (var connection = new RepositoryConnection())
    {
        using (var repositories = new Repository<Product>(connection))
        {
            repositories.Create(product);
            connection.Commit();
        }
    }
}