NHibernate将可空列映射为非空属性

huangapple go评论54阅读模式
英文:

NHibernate nullable column mapped to not null property

问题

我遇到了NHibernate映射的问题,当数据库字段可空,而开发人员忘记在相应的.NET实体中声明它为可空时。示例:

表:

CREATE TABLE myTable
(
   ID int NOT NULL,
   Total int NOT NULL,
   Discount int NULL --可空字段
)

INSERT INTO myTable VALUES (1, 10, NULL)

C# 实体:

public class MyTable
{
   public int ID { get; set; }
   public int Total { get; set; }
   public int Discount { get; set; } //错误地未声明为可空
}

NHibernate映射:

public class MyTableMap : ClassMap<MyTable>
{
    public MyTableMap()
    {
        Table("dbo.myTable");
        Id(x => x.ID).GeneratedBy.Assigned().Column("ID");
        Map(x => x.Total).Column("Total");
        Map(x => x.Discount).Column("Discount"); //将映射设置为 .Nullable() 不会改变行为
    }
}

当我尝试加载实体时:

session.Get<MyTable>(1);

我期望会收到异常,因为Discount字段为null,但实际上,NHibernate会默默地加载实体并将Discount字段的默认值设置为0,然后在第一次session.Flush()时更新数据库表,即使我没有更改实体的任何其他值。这在处理日期时间字段时会更糟,因为.NET DateTime的默认值是'01/01/0001',会导致异常:

将datetime2数据类型转换为datetime数据类型时导致数值超出范围

是否有其他人遇到了相同的问题?是否有SessionFactory的配置可以强制NHibernate在将NULL列映射到非可空.NET属性时抛出异常?要检查每个属性和列的每个映射,特别是在处理其他人的代码时,可能会很困难。

英文:

I'm having issues with wrong NHibernate mappings when there is a nullable database field and developer forget to declare it nullable in the corresponding .Net Entity. Example:

Table:

CREATE TABLE myTable
(
   ID int NOT NULL,
   Total int NOT NULL,
   Discount int NULL --Nullable field
)

INSERT INTO myTable VALUES (1, 10, NULL)

C# Entity:

public class MyTable{
   public int ID { get; set; }
   public int Total { get; set; }
   public int Discount { get; set; } //not declared as nullable by mistake
}

NHibernate Mapping:

public class MyTableMap : ClassMap&lt;MyTable&gt;
{
    public MyTableMap()
    {
        Table(&quot;dbo.myTable&quot;);
        Id(x =&gt; x.ID).GeneratedBy.Assigned().Column(&quot;ID&quot;);
        Map(x =&gt; x.Total).Column(&quot;Total&quot;);
        Map(x =&gt; x.Discount).Column(&quot;Discount&quot;); //setting mapping to .Nullable() doesn&#39;t change the behaviour
    }
}

When i try to load the entity:

session.Get&lt;MyTable&gt;(1);

I would expect to get an exception, because the Discount field is null, but instead, NHibernate silently load the entity with default value 0 and then updates the database table at the first session.Flush(), even if i don't change any other value of the entity. That's even worse with datetime fields, because default value of .Net DateTime is '01/01/0001' and i get the exception:

The conversion of a datetime2 data type to a datetime data type resulted in an out-of-range value

Did anyone else faced the same issue? Is there any configuration in SessionFactory to force NHibernate to throw an exception when a NULL column is mapped to non nullable .Net property? It's difficult to get fix it by checking every single mapping of every single property and column, especially when you work on someone else's code.

答案1

得分: 1

以下是代码的翻译部分:

在评论中提到的建议下,我编写了一些用于原始类型的约定,当数据库值为NULL且属性未声明为可为空时,在NullSafeGet方法上引发异常

例如,以下是应用于Int属性的自定义类型

public class IntNotNullableType : Int32Type
{
    public override object NullSafeGet(DbDataReader rs, string name, ISessionImplementor session)
    {
        //检查数据库返回的值是否为null,如果是,则引发异常。
        //不幸的是,列名类似col_2_0_,但您应该能够在异常日志中看到完整的查询,从而找到错误的映射。
        if (rs.IsDBNull(name))
            throw new NoNullAllowedException("列 " + name + " 返回了不可为空属性的NULL值");
        return base.NullSafeGet(rs, name, session);
    }
}

约定:

public class IntNotNullableTypeConvention : IPropertyConvention, IPropertyConventionAcceptance
{
    public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
    {
        criteria.Expect(x => x.Property.PropertyType == typeof(int) && Nullable.GetUnderlyingType(x.Property.PropertyType) == null); //应用于所有未声明为可空的int属性(int?或Nullable<int>)
    }

    public void Apply(IPropertyInstance instance)
    {
        instance.CustomType(typeof(IntNotNullableType));
    }
}

最后,在SessionFactory中添加该约定:

public static class SessionFactoryBuilder
{
    public static ISessionFactory Build(string connectionString)
    {
        return Fluently
            .Configure()
            .Database(() =>
            {
                return MsSqlConfiguration
                        .MsSql2012
                        .ConnectionString(connectionString);
            })
            .Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())
            .Conventions.Add<IntNotNullableTypeConvention>() //添加约定
           )
            .BuildSessionFactory();
    }
}

您可以为所有其他原始类型执行相同的操作,例如DateTime、bool、double等。只需创建新的类型和约定,继承自正确的类型。

日期时间示例:

public class DateTimeNotNullableType : DateTimeType
{
    public override object NullSafeGet(DbDataReader rs, string name, ISessionImplementor session)
    {
        if (rs.IsDBNull(name))
            throw new NoNullAllowedException("列 " + name + " 返回了不可为空属性的NULL值");
        return base.NullSafeGet(rs, name, session);
    }
}

public class DateTimeNotNullableTypeConvention : IPropertyConvention, IPropertyConventionAcceptance
{
    public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
    {
        criteria.Expect(x => x.Property.PropertyType == typeof(DateTime) && Nullable.GetUnderlyingType(x.Property.PropertyType) == null);
    }

    public void Apply(IPropertyInstance instance)
    {
        instance.CustomType(typeof(DateTimeNotNullableType));
    }
}

public static ISessionFactory Build(string connectionString)
{
    return Fluently
        .Configure()
        .Database(() =>
        {
            return MsSqlConfiguration
                        .MsSql2012
                        .ConnectionString(connectionString);
        })
        .Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())
        .Conventions.Add<IntNotNullableTypeConvention>() //添加约定
        .Conventions.Add<DateTimeNotNullableTypeConvention>()
        )
        .BuildSessionFactory();
}

希望对您有所帮助。

英文:

Following the suggestion in comments, i wrote a few conventions for primitive types that throws an exception on NullSafeGet method when database value is NULL and property is not declared as nullable.

For example, following is the custom type to apply to Int properties:

public class IntNotNullableType : Int32Type
{
    public override object NullSafeGet(DbDataReader rs, string name, ISessionImplementor session)
    {
        //Check if value returned by database is null, then throw an exception. 
        //Unfortunately column name is something like col_2_0_, but you should be able to see the full query in exception log, so you can find the wrong mapping
        if (rs.IsDBNull(name))
            throw new NoNullAllowedException(&quot;Column &quot; + name + &quot; returned NULL value for not nullable property&quot;);
        return base.NullSafeGet(rs, name, session);
    }
}

The convention:

public class IntNotNullableTypeConvention : IPropertyConvention, IPropertyConventionAcceptance
{
    public void Accept(IAcceptanceCriteria&lt;IPropertyInspector&gt; criteria)
    {
        criteria.Expect(x =&gt; x.Property.PropertyType == typeof(int) &amp;&amp; Nullable.GetUnderlyingType(x.Property.PropertyType) == null); //apply to all int properties NOT declared as nullable (int? or Nullable&lt;int&gt;)
    }

    public void Apply(IPropertyInstance instance)
    {
        instance.CustomType(typeof(IntNotNullableType));
    }
}

And finally add the convention in SessionFactory:

public static class SessionFactoryBuilder
{
    public static ISessionFactory Build(string connectionString)
    {
        return Fluently
            .Configure()
            .Database(() =&gt;
            {
                return MsSqlConfiguration
                        .MsSql2012
                        .ConnectionString(connectionString);
            })
            .Mappings(m =&gt; m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())
            .Conventions.Add&lt;IntNotNullableTypeConvention&gt;() //add the convention
           )
            .BuildSessionFactory();
    }
}

You can do the same for all other primitive types, such as DateTime, bool, double etc. Just create a new Type and Convention, inheriting from the correct type.

Example for datetime:

public class DateTimeNotNullableType : DateTimeType
{
    public override object NullSafeGet(DbDataReader rs, string name, ISessionImplementor session)
    {
        if (rs.IsDBNull(name))
            throw new NoNullAllowedException(&quot;Column &quot; + name + &quot; returned NULL value for not nullable property&quot;);
        return base.NullSafeGet(rs, name, session);
    }
}


public class DateTimeNotNullableTypeConvention : IPropertyConvention, IPropertyConventionAcceptance
{
    public void Accept(IAcceptanceCriteria&lt;IPropertyInspector&gt; criteria)
    {
        criteria.Expect(x =&gt; x.Property.PropertyType == typeof(DateTime) &amp;&amp; Nullable.GetUnderlyingType(x.Property.PropertyType) == null);
    }

    public void Apply(IPropertyInstance instance)
    {
        instance.CustomType(typeof(DateTimeNotNullableType));
    }
}


public static ISessionFactory Build(string connectionString)
{
    return Fluently
        .Configure()
        .Database(() =&gt;
        {
            return MsSqlConfiguration
                        .MsSql2012
                        .ConnectionString(connectionString);
        })
        .Mappings(m =&gt; m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())
        .Conventions.Add&lt;IntNotNullableTypeConvention&gt;() //add the convention
        .Conventions.Add&lt;DateTimeNotNullableTypeConvention&gt;()
        )
        .BuildSessionFactory();
 }

huangapple
  • 本文由 发表于 2023年2月8日 22:47:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/75387461.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定