Как мне справиться с тем, чтобы свойство сущностей не обнулялось, в то время как столбец обнуляем?- EF Core - PullRequest
2 голосов
/ 17 мая 2019

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

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

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

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

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

Я попытался изменить рефакторинг свойства на «PropertySafe» (чтобы не нарушать существующий код), затем создал новое «Свойство», которое было подключено, и разрешил получателю «PropertySafe» вызвать свойство и вернуть соответствующее значение по умолчанию, равное нулю. , Это лучше, так как остальная часть кода не должна знать об этом недостатке схемы базы данных, однако это громоздко делать снова и снова, и я нахожу это немного уродливым. Я также боюсь, что некоторые запросы 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 сможет быть необнуляемым, оставить столбец базы данных обнуляемым и обработчик обработчика структуры сущностей будет возвращать значение по умолчанию при обнаружении нуля, в то время как в настоящее время он разрушается при разборе null (в частности, в 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? различия также. Хотелось бы надеяться, что данное решение может быть применено между любыми типами данных, которые могут обнуляться / не обнуляться.

UPDATE:

У меня новое руководство. В классе SqlServerDateTimeMapping я могу переопределить метод:

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

и проверьте methodInfo. Конечно же, это метод «GetDateTime», тот самый, который падает при разборе. Мой мыслительный процесс, может быть, я могу каким-то образом вернуть свой собственный метод, который может быть вызван на SqlDataReader, который обрабатывает нулевую проверку, или подкласс SqlDataReader, чтобы переопределить этот метод. Теперь я заметил, что GetDateTime принимает в качестве параметра int, ссылаясь на какой столбец. Внутренне он вызывает IsDBNull перед вызовом, чтобы убедиться, что он не нулевой. Я надеялся, что передается строка, которую я могу переопределить, но похоже, что она просто берет столбец и использует SqlBuffer для get_dateTime on, то есть кто выполняет синтаксический анализ

1 Ответ

0 голосов
/ 18 мая 2019

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