当列可为空时,应如何处理使实体属性不可为空的问题? -EF核心

时间:2019-05-17 20:15:32

标签: c# entity-framework entity-framework-core

该数据库具有nullabble列,有人告诉我可以安全地假设其中永远不会包含null数据。实体框架代码是在这种假设下建模的。但是,我发现某些客户数据库实际上在这些列中确实具有偶尔的空值。实体框架在内部开始使用SqlReader读取这些列,例如通过对null值调用getDateTime来使其崩溃。

数据库模式是很久以前设计的,目前无法更新。简而言之,不应为空的列有时为空。该软件将这些列强制存储为适当的值,而不是数据库。

我们的实体框架代码是在假定软件已正确完成其工作的情况下进行建模的,而这些原本不应该为null的列根本就不是null。

在测试中,我发现两个客户的数据库以某种方式在不应包含的列中具有空值,并且实体框架在试图解析错误时在内部爆炸。这些都是历史性的数据库,仍然可以加载,但是可能是由于该软件在10年前更加漏洞百出所致。这些解析错误都是SqlReader错误,例如,由于无法为不可为null的DateTime对象的null值调用getDateTime。

我可以尝试炸毁它,然后手动将每个属性更改为可为空(然后更改代码以现在处理它可为空的机会),但是我不想使每个属性都为可为空。到目前为止,我见过的每个空值都可以默认为一个适当的值。

我尝试将一个属性重构为“ PropertySafe”(不破坏现有代码),然后制作了一个新的“ Property”,并将其连接起来,然后让“ PropertySafe”的getter调用Property并在null上返回适当的默认值。这样做更好,因为其余的代码不必知道此数据库架构缺陷,但是要一遍又一遍地做起来很麻烦,我觉得这有点丑陋。我还担心某些获取了这些“安全”属性之一的EFContext查询会混淆EF并迫使其在客户端运行它们,这会降低性能。

我深入研究了RelationalTypeMapping和ValueConverter,认为我可以处理从sql的“ datetime not null”列到CLR DateTime属性的问题,这在我需要时默认为默认值,但是由于数据库已经被解析而无法正常工作在那时候。我将需要覆盖专门在SqlReader上调用getDateTime的任何地方,并在那里拦截它。

失败的表/列实体/属性示例与以下示例实质上相同。

public class Entity {
    public int Id { get; set; }
    public DateTime DueDate { get; set; }

    public class Configuration : BaseConfiguation<Entity> {
      protected override string TableName => "ENTITY_DUE_DATE";
      protected override void DoConfigure( EntityTypeBuilder<Entity> builder ) {
        builder.HasKey( x => new { x.Id } );
        builder.Property( x => x.Id ).HasColumnName( "ID" );
        builder.Property( x => x.DueDate ).HasColumnName( "DUEDATE" );
      }
    }
  }

使用SQL模式

CREATE TABLE ENTITY_DUE_DATE  (
    ID INT NOT NULL,
    DUEDATE DATETIME NULL
);

DateTime映射的外观

public class SqlServerDateTimeMapping : DateTimeTypeMapping {

    public SqlServerDateTimeMapping() : base( "DateTime", System.Data.DbType.DateTime ) { }

    protected SqlServerDateTimeMapping( RelationalTypeMappingParameters parameters ) : base( parameters ) { }

    public override string GenerateSqlLiteral( object value ) {
      if ( value is DateTime date ) {
        if ( date.Date != date ) {
          System.Diagnostics.Debug.Fail( "Time portions are not supported" );
        }
        return date.ToString( "yyyy-MM-dd" );
      }
      return base.GenerateSqlLiteral( value );
    }

    public override RelationalTypeMapping Clone( in RelationalTypeMappingInfo mappingInfo ) => new SqlServerDateTimeMapping();

    public override RelationalTypeMapping Clone( string storeType, int? size ) => new SqlServerDateTimeMapping();

    public override CoreTypeMapping Clone( ValueConverter converter ) => new SqlServerDateTimeMapping();
  }

正如我所说,我试图制作一个转换器以插入SqlServerDateTimeMapping的Converter覆盖中,但是我不知道如何在Sql解析之前/期间对其进行处理。看来转换器可以让您在两种CLR类型之间进行转换,因此可以进行后解析。

为了完整起见,这是我目前的转换器。但这是完全错误的。

public class NullableDateTimeToDateTimeConverter : ValueConverter<DateTime?, DateTime> {
    public NullableDateTimeToDateTimeConverter() : base(s => s ?? DateTime.MaxValue, x => x ) { }
  }

我希望能够使我的Entity类的Property为不可为空,使数据库列为可为空,并让实体框架引擎在找到null时返回默认值,而目前在解析null(特别是在任何Context查询中,由SqlReader.GetDateTime()调用的SqlBuffer.GetDateTime()中,无论其context.EntityDueDate.ToList()还是context.Set()。)

无论解决方案是什么,如果我的查询仍然可以处理数据库端而不是客户端端,那将是理想的选择。所以如果我做了

context.EntityDueDates.Where(x => x.DueDate > DateTime.UtcNow).ToList()

最好能够在数据库上运行该查询,而不必返回客户端进行

DueDate ?? DEFAULT_VALUE

逻辑,但是适合其中。

我应该澄清一下,该解决方案应该不仅适用于DateTime。我之所以选择DateTime,是因为我手动修复的是DateTime,但是检查数据库显示出这种相同的情况可能会发生几个int / int吗?差异也是如此。我希望给出的解决方案可以在任何可空/不可空数据类型之间应用。

更新:

我有一个新的领导。在SqlServerDateTimeMapping类中,我可以重写该方法:

public override MethodInfo GetDataReaderMethod() {
      var methodInfo = base.GetDataReaderMethod();
      return methodInfo;
}

并检查methodInfo。果然,这就是方法“ GetDateTime”,与解析时崩溃的方法相同。我的想法是,也许我可以以某种方式返回我自己的方法,该方法可以在处理空检查的SqlDataReader上调用,也可以返回SqlDataReader的子类来重写此方法。我现在注意到,尽管GetDateTime将int作为参数,指的是哪一列。在内部,它在调用以确保它不为null之前调用IsDBNull。我希望传递一个可以覆盖其解析的字符串,但是看起来这只不过是占用一列并使用SqlBuffer来进行get_dateTime,由谁进行解析

1 个答案:

答案 0 :(得分:0)

我已经解决了答案,但是我愿意接受其他解决方案。这“解决了”我的答案,但是将我限制为每个类型只有一个通用的“默认”。有时“最佳”默认值可能是DateTime.MinValue,有时是DateTime.MaxValue,有时是DateTime.UtcNow。违约有其缺陷。我确实认为此答案不会对性能造成影响,并且我相信不会以任何可能使工作在查询的客户端完成的方式弄乱映射。

我的解决方案是创建一个类“ ContextDbDataReaderDecorator”,该类可以在DbDataReader之间隐式地来回转换。它在创建时存储DbDataReader,并将其用于自己的GetDataTimeSafe替代方法。隐式转换使我们可以从SqlServerDateTimeMapping的“ GetDataReaderMethod”重载中返回它,因为尽管它实际上并不属于DbDataReader,但仍可以在DbDataReader上对其进行调用。

我的关系类型映射对象如下所示:

public class SqlServerDateTimeMapping : DateTimeTypeMapping {
    static MethodInfo s_getDateTimeSafeMethod = typeof( ContextDbDataReaderDecorator ).GetMethod( nameof( ContextDbDataReaderDecorator.GetDateTimeSafe ) );

    public SqlServerDateTimeMapping() : base( "DateTime", System.Data.DbType.DateTime ) {}

    protected SqlServerDateTimeMapping( RelationalTypeMappingParameters parameters ) : base( parameters ) {}

    public override string GenerateSqlLiteral( object value ) {
      if ( value is DateTime date ) {
        if ( date.Date != date ) {
          System.Diagnostics.Debug.Fail( "Time portions are not supported" );
        }
        return date.ToString( "yyyy-MM-dd" );
      }
      return base.GenerateSqlLiteral( value );
    }


    public override MethodInfo GetDataReaderMethod() {
      return s_getDateTimeSafeMethod;
    }

    public override RelationalTypeMapping Clone( in RelationalTypeMappingInfo mappingInfo ) => new SqlServerDateTimeMapping();
    public override RelationalTypeMapping Clone( string storeType, int? size ) => new SqlServerDateTimeMapping();
    public override CoreTypeMapping Clone( ValueConverter converter ) => new SqlServerDateTimeMapping();
  }

以及我创建的ContextDbDataReaderDecorator类如下所示:

public class ContextDbDataReaderDecorator {
    public DbDataReader DbDataReader { get; }

    public ContextDbDataReaderDecorator( DbDataReader inner ) {
      DbDataReader = inner;
    }

    public static implicit operator DbDataReader( ContextDbDataReaderDecorator self )  => self.DbDataReader;
    public static implicit operator ContextDbDataReaderDecorator( DbDataReader other ) => new ContextDbDataReaderDecorator( other );
    public DateTime GetDateTimeSafe( int ordinal ) => (DbDataReader.GetValue( ordinal ) as DateTime?) ?? new DateTime( 3000, 1, 1 );
    public int GetIntSafe(int ordinal) => (DbDataReader.GetValue( ordinal ) as int?) ?? 0;
    public long GetLongSafe( int ordinal ) => (DbDataReader.GetValue( ordinal ) as long?) ?? 0;
    public float GetFloatSafe(int ordinal) => (DbDataReader.GetValue( ordinal ) as float?) ?? 0.0f;
  }