You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NHibernate多对一级联异常:更新触发DELETE/INSERT而非UPDATE

问题:NHibernate更新BankingProvider时总是删插中间表CasinoBankingProvider记录

我有BankingProvider对象,与CasinoBankingProvider对象集合是一对多关系,CasinoBankingProvider作为中间表关联Casino对象(中间表包含额外属性)。现在更新BankingProvider时,NHibernate会先删除再插入关联的CasinoBankingProvider记录,即使中间表的属性只是变更,也不会执行UPDATE操作。

加载BankingProvider页面时NHibernate能生成正确的JOIN SQL,说明基础映射没问题,但尝试调整各种Cascade组合都无效,始终生成DELETE+INSERT语句。


现有映射代码

BankingProviderMap

public BankingProviderMap()
{
    Table("BankingProvider");

    #region Casinos
    Bag(x => x.Casinos, intersectTableMap =>
    {
        intersectTableMap.Cascade(Cascade.Merge);
        intersectTableMap.Table("CasinoBankingProvider");
        intersectTableMap.Key(key =>
        {
            key.Column("BankingProviderId");
            key.ForeignKey("FK_CasinoBankingProvider_BankingProvider");
        });
    },
    map => map.ManyToMany(p => p.Column("CasinoId")));
...
...
}

CasinoBankingProviderMap

public CasinoBankingProviderMap()
{
    Table("CasinoBankingProvider");
    ManyToOne(x => x.Casino, manyToOneMap =>
    {
        manyToOneMap.Cascade(Cascade.All | Cascade.None | Cascade.Persist | Cascade.DeleteOrphans);
        manyToOneMap.Class(typeof(Casino));
        manyToOneMap.Column("CasinoId");
        manyToOneMap.ForeignKey("FK_CasinoBankingProvider_Casino");
    });

    ManyToOne(x => x.BankingProvider, manyToOneMap =>
    {
        manyToOneMap.Cascade(Cascade.None);
        manyToOneMap.Class(typeof(BankingProvider));
        manyToOneMap.Column("BankingProviderId");
        manyToOneMap.ForeignKey("FK_CasinoBankingProvider_BankingProvider");
    });

    Property(x => x.MinimumDepositAmount);
    Property(x => x.DepositFeeAmount);
    Property(x => x.DepositFeePercent);
    Property(x => x.DepositPeriodMode);
    Property(x => x.DepositPeriodApproximateHours);
    Property(x => x.DepositPeriodRangeStartHours);
    Property(x => x.DepositPeriodRangeEndHours);
    Property(x => x.MinimumWithdrawalAmount);
    Property(x => x.WithdrawalFeeAmount);
    Property(x => x.WithdrawalFeePercent);
    Property(x => x.WithdrawalPeriodMode);
    Property(x => x.WithdrawalPeriodApproximateHours);
    Property(x => x.WithdrawalPeriodRangeStartHours);
    Property(x => x.WithdrawalPeriodRangeEndHours);

}

CasinoMap

public CasinoMap()
{
    Table("Casino");
    DynamicUpdate(true);

    #region BankingProviders
    Bag(x => x.CasinoBankingProviders, oneToManyMap =>
    {
        oneToManyMap.Cascade(Cascade.Refresh | Cascade.DeleteOrphans);
        oneToManyMap.Table("CasinoBankingProvider");
        oneToManyMap.Key(key =>
        {
            key.Column("CasinoId");
            key.ForeignKey("FK_Casino_BankingProvider_Casino");
        });
    },
    map => map.OneToMany());

    #endregion
...
...
}

生成的SQL(无论Cascade如何配置)

exec sp_executesql N'UPDATE BankingProvider SET DateAdded = @p0, DateUpdated = @p1, Name = @p2, IsActive = @p3, IsDefaultProvider = @p4, CanDeposit = @p5, CanWithdraw = @p6, IsMobileCompatible = @p7, PaymentMedium = @p8, Description = @p9, PaymentType = @p10, MinimumDepositAmount = @p11, MaximumDepositAmount = @p12, DepositFeeAmount = @p13, DepositFeePercent = @p14, WithdrawalTurnaroundMode = @p15, WithdrawalTurnaroundApproximateHours = @p16, WithdrawalTurnaroundRangeStartHours = @p17, WithdrawalTurnaroundRangeEndHours = @p18, MinimumWithdrawalAmount = @p19, MaximumWithdrawalAmount = @p20, WithdrawalFeeAmount = @p21, WithdrawalFeePercent = @p22, DepositTurnaroundMode = @p23, DepositTurnaroundApproximateHours = @p24, DepositTurnaroundRangeStartHours = @p25, DepositTurnaroundRangeEndHours = @p26 WHERE Id = @p27',N'@p0 datetime,@p1 datetime,@p2 nvarchar(4000),@p3 bit,@p4 bit,@p5 bit,@p6 bit,@p7 bit,@p8 nvarchar(4000),@p9 nvarchar(4000),@p10 int,@p11 decimal(28,5),@p12 decimal(28,5),@p13 decimal(28,5),@p14 decimal(28,5),@p15 int,@p16 int,@p17 int,@p18 int,@p19 decimal(28,5),@p20 decimal(28,5),@p21 decimal(28,5),@p22 decimal(28,5),@p23 int,@p24 int,@p25 int,@p26 int,@p27 int',@p0='2024-06-13 13:05:52',@p1='2025-04-14 10:55:15',@p2=N'1inch Crypto',@p3=1,@p4=0,@p5=0,@p6=0,@p7=0,@p8=NULL,@p9=NULL,@p10=0,@p11=0,@p12=0,@p13=0,@p14=0,@p15=1,@p16=NULL,@p17=NULL,@p18=NULL,@p19=0,@p20=0,@p21=0,@p22=0,@p23=1,@p24=NULL,@p25=NULL,@p26=NULL,@p27=699
go
exec sp_executesql N'DELETE FROM CasinoBankingProvider WHERE BankingProviderId = @p0',N'@p0 int',@p0=699
go
exec sp_executesql N'INSERT INTO CasinoBankingProvider (BankingProviderId, CasinoId) VALUES (@p0, @p1)',N'@p0 int,@p1 int',@p0=699,@p1=2218
go

问题原因及解决方案

核心原因

你现在用的是ManyToMany映射来处理BankingProvider和Casino的关联,但NHibernate的ManyToMany只适用于无额外字段的纯关联表。当中间表CasinoBankingProvider包含额外属性(比如MinimumDepositAmount、DepositFeeAmount等)时,这种映射方式本质上是把中间表当成了“纯关联容器”,NHibernate无法识别中间表的实体属性变更,只会通过删插来维护关联关系。

另外,你在CasinoMap里又把CasinoBankingProviders映射成了OneToMany,这种混合映射会导致NHibernate的状态跟踪混乱,进一步加剧问题。

修复步骤

  1. 修改BankingProvider的映射,将ManyToMany改为OneToMany关联CasinoBankingProvider
    把BankingProvider中的Casinos集合(当前是ManyToMany到Casino)改成CasinoBankingProviders集合(OneToMany到中间表实体):

    public BankingProviderMap()
    {
        Table("BankingProvider");
    
        // 替换原来的ManyToMany Casinos集合
        Bag(x => x.CasinoBankingProviders, map =>
        {
            map.Cascade(Cascade.All | Cascade.DeleteOrphans);
            map.Key(key =>
            {
                key.Column("BankingProviderId");
                key.ForeignKey("FK_CasinoBankingProvider_BankingProvider");
            });
        },
        map => map.OneToMany());
    }
    
  2. 调整实体类结构
    确保BankingProvider类中不再直接包含Casino集合,而是包含CasinoBankingProvider类型的集合,通过中间表实体来访问关联的Casino:

    public class BankingProvider
    {
        // 替换原来的ICollection<Casino> Casinos
        public virtual ICollection<CasinoBankingProvider> CasinoBankingProviders { get; set; }
    }
    
  3. 确保双向关联的一致性
    在Casino实体中保留CasinoBankingProviders的OneToMany映射,但要确保关联的双向同步(比如在添加/移除中间表实体时,同时设置对应的BankingProvider和Casino引用)。

  4. 调整Cascade配置

    • 对于BankingProvider的CasinoBankingProviders集合,使用Cascade.All | Cascade.DeleteOrphans,这样NHibernate会自动处理中间表实体的增删改。
    • CasinoBankingProvider中的ManyToOne关联(到BankingProvider和Casino)保持Cascade.None即可,因为这两个实体应该独立维护。

为什么之前的Cascade配置无效?

因为你用了ManyToMany映射,这种映射模式下,NHibernate不会把中间表当作独立实体来跟踪,只会处理关联关系的存在与否,完全忽略中间表的属性变更。无论怎么调整Cascade,NHibernate都只会用删插来维护关联,不会生成UPDATE语句。只有把中间表映射成独立实体,用OneToMany+ManyToOne的双向关联,NHibernate才能跟踪中间表的属性变化,生成对应的UPDATE语句。


内容的提问来源于stack exchange,提问作者Adam Hey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 11:10:05