Возможно ли для EF Core 3.0 использовать FromSql внутри ModelBuilder, а не в DbSet <> - PullRequest
0 голосов
/ 08 октября 2019

Я сопоставляю EF с устаревшей БД, и на данный момент для этого нужно создать представления в устаревшей БД.

Вместо того, чтобы вообще изменять существующую схему, я хотел бы использоватьновые HasNoKey существующие FromSql методы для сопоставления моих сущностей с SQL, определенным в моем приложении.

то есть

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Names>(entity =>
  {
    entity.HasNoKey();
    entity.FromSql("{SELECT_STATEMENT_FROM_MY_VIEW}");
    ... OR ...
    entity.ToView("{SELECT_STATEMENT_FROM_MY_VIEW_NOT_VIEW_NAME}")
  });

}

Я думаю, я мог бы сделать это в новом IInterceptorно это кажется мне немного хакерским.

1 Ответ

0 голосов
/ 08 октября 2019

В конечном итоге я использовал IInterceptor - в основном он предполагает, что в T-SQL представление всегда эквивалентно (только для чтения) выбору и поэтому может быть напрямую заменено.

Это базовый класс расширений, который генерирует регистры синглтона-перехватчика и отслеживает SQL для замены, вставляя основанное на guid имя таблицы «mock» с использованием существующего ToView. Затем перед запуском текста команды он заменяет несуществующее представление связанным SQL:

public static class DbContextExtensions
{
    private static readonly SqlViewInterceptor SqlViewInterceptorSingleton = new SqlViewInterceptor();

    public static DbContextOptionsBuilder AddViewToSqlInterceptor(
        this DbContextOptionsBuilder dbContextOptionsBuilder)
    {
        dbContextOptionsBuilder.AddInterceptors(SqlViewInterceptorSingleton);
        return dbContextOptionsBuilder;
    }

    public static EntityTypeBuilder<T> ToSqlView<T>(this EntityTypeBuilder<T> entityTypeBuilder, string sql)
        where T : class
    {
        return entityTypeBuilder.ToView(SqlViewInterceptorSingleton.RegisterSqlForView(sql));
    }

    private class SqlViewInterceptor : DbCommandInterceptor
    {
        static readonly ConcurrentDictionary<string, string> MockTablesToSql = new ConcurrentDictionary<string, string>();

        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
        {
            foreach (var mockTable in MockTablesToSql.Keys)
            {
                command.CommandText = command.CommandText.Replace(mockTable, MockTablesToSql[mockTable]);
            }

            return base.ReaderExecuting(command, eventData, result);
        }

        public string RegisterSqlForView(string viewSql)
        {
            var mockTableName = Guid.NewGuid().ToString();
            MockTablesToSql.TryAdd($"[{mockTableName}]", $"({viewSql})");
            return mockTableName;
        }
    }
}

Затем мы можем использовать стандарт DbContext обычным способом - необходимо убедиться, что перехватчик зарегистрирован вOnConfiguring, и затем можно использовать расширение AddViewToSql для регистрации. Просмотр эквивалентного SQL только для чтения:

public class LegacyDbContext : DbContext
{

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer("data source=.\\sql2017; database=Test; integrated security=true")
            .AddViewToSqlInterceptor();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Arbitrary SQL for parent
        modelBuilder
            .Entity<LegacyEntity>()
            .ToSqlView("SELECT CASE LegacyId WHEN 100 THEN 1 ELSE LegacyId END LegacyId FROM LegacyTable");

        // Arbitrary SQL for child
        modelBuilder
            .Entity<LegacyChild>()
            .ToSqlView("SELECT LegacyParentId LegacyEntityLegacyId, LegacyChildId FROM LegacyChild");
    }

    public DbSet<LegacyEntity> LegacyEntities { get; set; }
}

public class LegacyEntity
{
    [Key]
    public int LegacyId { get; set; }

    public IList<LegacyChild> Children { get; set;  }
}

public class LegacyChild
{
    public int LegacyChildId { get; set; }
}

И вот несколько модульных тестов, которые я написал, чтобы подтвердить это (по крайней мере для простых случаев) поведение соответствует ожидаемому - EF по-прежнему будет ограничивать с помощью предложений WHERE и агрегировать с помощью SUM в SQL, что позволит Include работать для отношений родитель-потомок ...

[TestFixture]
public class TestDbContext
{
    [SetUp]
    public void SetUp()
    {
        using var ctx = new LegacyDbContext();
        ctx.Database.ExecuteSqlRaw("TRUNCATE TABLE LegacyTable;");
        ctx.Database.ExecuteSqlRaw("TRUNCATE TABLE LegacyChild;");
        for (var i = 1; i < 10; i++)
        {
            ctx.Database.ExecuteSqlRaw($"INSERT INTO LegacyTable (LegacyId) VALUES ({i});");
            ctx.Database.ExecuteSqlRaw($"INSERT INTO LegacyChild (LegacyParentId, LegacyChildId) VALUES ({i}, {i * 2});");
        }
    }

    [Test]
    public void TestLegacyView()
    {
        using var ctx = new LegacyDbContext();
        var filteredRows = ctx.LegacyEntities.Where(x=>x.LegacyId <= 5).ToArray();
        Assert.That(filteredRows.Length, Is.EqualTo(5));
    }

    [Test]
    public void TestLegacyViewScalar()
    {
        using var ctx = new LegacyDbContext();
        var filteredRows = ctx.LegacyEntities.Where(x=>x.LegacyId <= 5).Sum(x=>x.LegacyId);
        Assert.That(filteredRows, Is.EqualTo(15));
    }

    [Test]
    public void TestLegacyChild()
    {
        using var ctx = new LegacyDbContext();
        var filteredRows = ctx.LegacyEntities
            .Include(x=>x.Children)
            .Where(x => x.LegacyId <= 5)
            .ToArray()
            .Sum(x => x.Children.Sum(c=>c.LegacyChildId));

        Assert.That(filteredRows, Is.EqualTo(30));
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...