Как мне справиться с тем, чтобы свойство сущностей не обнулялось, в то время как столбец обнуляем?

В базе данных есть столбцы, которые можно обнулять, и мне сказали, что можно с уверенностью предположить, что в них никогда не будет нулевых данных. Код структуры сущности был смоделирован в соответствии с этим предположением. Однако я обнаружил, что некоторые базы данных клиентов в этих столбцах иногда имеют нулевое значение. Entity Framework продолжает разрушаться при использовании SqlReader для чтения этих столбцов, например, вызывая getDateTime со значением NULL.

Схема базы данных была разработана давно, и в настоящее время не может быть обновлена. Короче говоря, столбцы, которые не должны быть нулевыми, иногда бывают нулевыми. Программное обеспечение заставляет эти столбцы хранить соответствующие значения, а не базу данных.

Наш код Entity Framework был смоделирован, предполагая, что программное обеспечение правильно выполнило свою работу, и что эти столбцы, которые не должны были быть нулевыми, просто не были нулевыми.

В ходе тестирования я обнаружил две клиентские базы данных, которые каким-то образом имеют нулевые значения в столбцах, которых не должно быть, и структура сущностей взорвалась внутренне при попытке проанализировать ошибки. Это исторические базы данных, которые все еще загружаются, но, вероятно, были вызваны тем, что программное обеспечение было более глючным 10+ лет назад. Все эти ошибки синтаксического анализа являются ошибками SqlReader, например, из-за невозможности вызова getDateTime для нулевого значения для объекта DateTime, который не может иметь значение NULL.

Я могу использовать метод проб и ошибок там, где оно взорвется, и вручную изменить каждое свойство на обнуляемое (а затем изменить код, чтобы теперь обрабатывать вероятность его обнуления), однако я не хочу делать каждое свойство обнуляемым. Каждое нулевое значение, которое я видел до сих пор, могло быть установлено по умолчанию на соответствующее значение.

Я попытался сделать рефакторинг свойства «PropertySafe» (чтобы не нарушать существующий код), затем создал новое «Property», которое было подключено, и разрешил получателю «PropertySafe» вызвать свойство и вернуть соответствующее значение по умолчанию, равное null. Это лучше, так как остальная часть кода не должна знать об этом недостатке схемы базы данных, однако это громоздко делать снова и снова, и я нахожу это немного уродливым. Я также боюсь, что некоторые запросы EFContext, которые захватывают одно из этих «безопасных» свойств, могут запутать EF и заставить его запускать их на стороне клиента, что будет гораздо менее производительным.

Я совершил погружение в RelationalTypeMapping и ValueConverter, подумав, что, возможно, я мог бы справиться с переходом из столбцов sql "datetime not null" к свойству CLR DateTime, по умолчанию когда мне нужно, однако это не работает, потому что база данных уже была проанализирована в этот момент , Мне нужно переопределить, где бы он ни вызывал getDateTime в SqlReader, и перехватить его там.

Пример сбоя сущности / свойства таблицы / столбца практически такой же, как в следующем примере.

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, но я не мог понять, как с этим справиться ДО / В ТЕЧЕНИЕ парсинга Sql. Кажется, конвертер позволяет вам конвертировать между двумя типами CLR, так что пост-разбор.

Ради полноты, это мой конвертер в настоящее время. Это совершенно неправильно, хотя.

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

Я ожидаю, что у моего класса Entity свойство будет не обнуляемым, оставит столбец базы данных обнуляемым и обработчик обработчика структуры сущностей будет возвращать значение по умолчанию, когда он найдет ноль, в то время как он в настоящее время взрывается при разборе нуля (особенно в SqlBuffer.GetDateTime (), вызываемом SqlReader.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, ссылаясь на какой столбец. Внутренне он вызывает IsDBNull перед вызовом, чтобы убедиться, что он не нулевой. Я надеялся, что передается строка, которую я могу переопределить, но похоже, что это просто берет столбец и использует SqlBuffer для get_dateTime, то есть кто выполняет синтаксический анализ

Всего 1 ответ


Я решил ответ, однако я открыт для других решений. Это «решает» мой ответ, однако ограничивает меня наличием одного общего «По умолчанию» для каждого типа. Иногда «лучшим» значением по умолчанию может быть 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;
  }

Есть идеи?

10000