Фильтры таблиц Entity Framework Core (если таблица используется более чем один раз)

Я унаследовал базу данных SQL Server, где исходный разработчик использовал одну таблицу для представления всех типов "поиска" вместо того, чтобы указывать таблицу для каждого ...

create table Lookup (
    LookupID int,
    LookupType int,
    LookupKey int,
    LookupValue varchar(50)
)

Затем таблица используется (например) для предоставления различных списков на основе LookupType, чтобы вы могли видеть такие данные, как ...

ID     Type     Key     Value
1      1        1       Mr
2      1        2       Mrs
3      1        3       Miss
4      2        1       Dog
5      2        2       Cat
6      2        3       Hamster

Мне нужно использовать эту таблицу с Entity Framework Core, поскольку я хочу иметь возможность возвращать значения поиска при запросе данных. Возьмем, к примеру, следующую таблицу ...

create table Customer (
    CustomerID int,
    CustomerTitleID int, <- LookupType = 1
    PetTypeID int -- LookupType = 2
)

И данные будут выглядеть так ...

ID     TitleID     PetTypeID
1      1           1

Я могу определить класс «Поиск» ...

public class Lookup {
    public int LookupID {get; set;}
    public int LookupTypeID {get; set;}
    public int LookupKey {get; set;}
    public string LookupValue {get; set;}
} 

И я могу определить класс «Клиент» ...

public class Customer {
    public int CustomerID {get; set;}
    public Lookup CustomerTitle {get; set;}
    public int CustomerTitleID {get; set;}
    public Lookup PetType {get; set;}
    public int PetTypeID {get; set;}
}

Проблема в том, что хотя (в DbContext.OnModelCreating) я могу указать «Принципиальный ключ» для отношений «Клиент / Поиск» ...

entity<Customer>().HasOne<Lookup>(c => c.CustomerTitle).WithMany().WithForeignKey(c => c.CustomerTitleID).WithPrincipleKey(l => l.LookupKey);
entity<Customer>().HasOne<Lookup>(c => c.PetType).WithMany().WithForeignKey(c => c.PetTypeID).WithPrincipleKey(l => l.LookupKey);

Я не могу найти способ установить фильтр для каждого класса «Lookup», чтобы ограничить его «LookupTypeID».

Я попытался создать собственный класс «PetType» и связать его с «Lookup» вместе с фильтром (в DbContext.OnModelCreating) ...

entity.HasQueryFilter(lookup => lookup.LookupType == 2);

однако EF не хочет, чтобы с таблицей было связано более одного типа сущности, если только типы сущностей также не связаны («PetType» должен быть унаследован от «Lookup»).

Затем я унаследовал этот настраиваемый класс «PetType» от «Lookup» и попробовал тот же фильтр ...

entity.HasQueryFilter(petType=> petType.LookupType == 2);

но EF разрешит такой фильтр только на корневом уровне.

Я также пробовал использовать представления, однако, хотя объект DbSet может быть дочерним свойством объекта DbQuery, похоже, что это не работает наоборот.

Я скучаю по другому пути? Конечный результат, которого я пытаюсь достичь, это ...

from customer in dbContext.Customers
    .Select new
    {
        customer.CustomerID,
        Title = customer.CustomerTitle.LookupValue,
        PetType = customer.PetType.LookupValue
    }

и EF автоматически применяет фильтр к каждому поиску (очевидно, указанному мной), чтобы выбрать правильные строки.


person Martin Robins    schedule 08.08.2019    source источник


Ответы (1)


На самом деле вы можете обойти эту «схему», создав иерархию наследования TPH в EF Core. например:

using Microsoft.EntityFrameworkCore;
using System;
using System.Data;
using System.Linq;

namespace EfCoreTest
{

    public abstract class Lookup
    {
        public int LookupID { get; set; }
        public int LookupTypeID { get; set; }
        public int Key { get; set; }
        public string Value { get; set; }
    }

    public class CustomerTitle : Lookup
    {

    }
    public class PetType : Lookup
    {

    }
    public class Customer
    {
        public int CustomerID { get; set; }
        public CustomerTitle CustomerTitle { get; set; }
        public int CustomerTitleID { get; set; }
        public PetType PetType { get; set; }
        public int PetTypeID { get; set; }
    }


    public class Db : DbContext
    {
        readonly string connectionString;

        public DbSet<Customer> Customers { get; set; }
        public DbSet<Lookup> Lookup { get; set; }
        public DbSet<CustomerTitle> CustomerTitles { get; set; }
        public DbSet<PetType> PetTypes { get; set; }

        public Db(): this("server=.;database=EfCoreTest;Integrated Security=true")
        {

        }
        public Db(string connectionString)
        {
            this.connectionString = connectionString;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(connectionString);
            base.OnConfiguring(optionsBuilder);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Lookup>()
                .HasAlternateKey(e => new { e.LookupTypeID, e.Key });

            modelBuilder.Entity<Lookup>()
                .HasDiscriminator(e => e.LookupTypeID);

            modelBuilder.Entity<CustomerTitle>()
                .HasDiscriminator()
                .HasValue(1);
            modelBuilder.Entity<PetType>()
                .HasDiscriminator()
                .HasValue(2);

            var fks = from et in modelBuilder.Model.GetEntityTypes()
                      from fk in et.GetForeignKeys()
                      where typeof(Lookup).IsAssignableFrom(fk.PrincipalEntityType.ClrType)
                      select fk;
            foreach (var fk in fks)
            {
                fk.DeleteBehavior = DeleteBehavior.Restrict;
            }

            base.OnModelCreating(modelBuilder);
        }
    }



    class Program
    {
        static void Main(string[] args)
        {


            using (var db = new Db())
            {
                db.Database.EnsureDeleted();
                db.Database.EnsureCreated();

                var Mr = new CustomerTitle() { Key = 1, Value = "Mr" };
                var Mrs = new CustomerTitle() { Key = 2, Value = "Mrs" };
                var Miss = new CustomerTitle() { Key = 3, Value = "Miss" };
                db.CustomerTitles.AddRange( new []{ Mr,Mrs,Miss});

                var Dog = new PetType() { Key = 1, Value = "Dog" };
                var Cat = new PetType() { Key = 2, Value = "Cat" };
                var Hamster = new PetType() { Key = 3, Value = "Hamster" };
                db.PetTypes.AddRange(new[] { Dog,Cat,Hamster });

                db.SaveChanges();
            }

            using (var db = new Db())
            {
                var titles = db.CustomerTitles.ToDictionary(t => t.Value);
                var petTypes = db.PetTypes.ToDictionary(t => t.Value);

                var cust = new Customer();
                cust.CustomerTitle = titles["Mr"];
                cust.PetType = petTypes["Hamster"];

                db.SaveChanges();
            }


            Console.WriteLine("Hit any key to exit");
            Console.ReadKey();


        }
    }
}

В качестве альтернативы вы можете создать представления для поиска, например:

create or alter view CustomerTitle
as
select [Key] CustomerTitleId, Value
from Lookup
where LookupTypeID = 1
person David Browne - Microsoft    schedule 08.08.2019
comment
Спасибо, но я получаю System.InvalidOperationException: «Свойство дискриминатора не может быть установлено для типа сущности CustomerTitle, потому что оно не является корнем иерархии наследования». (Я использую EF Core 2.2.4, если это важно). - person Martin Robins; 08.08.2019
comment
Виноват; включил имя свойства в HasDiscriminator класса CustomerTitle. Исправлено, но теперь я получаю следующее: System.InvalidOperationException: «Невозможно установить значение дискриминатора« 2 »для свойства дискриминатора« Дискриминатор », поскольку оно не может быть присвоено свойству типа« System.String ».»; даже если свойство имеет тип int, а значение, которое я устанавливаю, - int. - person Martin Robins; 08.08.2019
comment
Попробуйте вставить этот образец в пустой проект и запустить его перед интеграцией в существующий проект. - person David Browne - Microsoft; 08.08.2019
comment
Только что закончил (решил, что это моя ошибка); все работает так, как вы ожидаете. Не уверен, что я понимаю, как EF узнает ключ поиска вместо LookupID - это конфигурация, основанная на соглашении, и если да, то есть ли какая-то документация, на которую вы можете указать мне? Тем не менее, я все еще не могу заставить его работать в моем собственном проекте - я вернусь к вам, как только узнаю, почему. - person Martin Robins; 08.08.2019
comment
Он ищет LookupID, а не Key. например, Customer.CustomerTitleID - ›Lookup.LookupID. - person David Browne - Microsoft; 08.08.2019
comment
У клиента должны быть и CustomerTitleID, и CustomerTitleLookupType для ссылки на Lookup.Key, если Lookup.Key не является альтернативным ключом. - person David Browne - Microsoft; 08.08.2019
comment
Ах; это снова его ломает. Первоначальная проблема (возможно, я не совсем понял это) заключалась в том, что унаследованный мной дизайн использует LookupKey в качестве значения, хранящегося в столбцах CustomerTitleID и PetTypeID, а не LookupID, как можно было бы ожидать. Я надеялся, что смогу преодолеть это с помощью комбинации того, что вы сделали с дискриминатором выше, и моей собственной игры с HasPrincipleKey, но если я попытаюсь настроить модель с помощью основных ключей, я получу исключение при первоначальном заполнении данных ... - person Martin Robins; 08.08.2019
comment
Я подумал: modelBuilder.Entity ‹Customer› () .HasOne ‹CustomerTitle› (e = ›e.CustomerTitle) .WithMany (). HasForeignKey (c =› c.CustomerTitleID) .HasPrincipalKey (ct = ›ct.Key); modelBuilder.Entity ‹Customer› () .HasOne ‹PetType› (e = ›e.PetType) .WithMany (). HasForeignKey (c =› c.PetTypeID) .HasPrincipalKey (ct = ›ct.Key); - person Martin Robins; 08.08.2019
comment
но System.InvalidOperationException: 'Экземпляр типа сущности' PetType 'не может быть отслежен, потому что другой экземпляр со значением ключа' {Key: 1} 'уже отслеживается. При присоединении существующих объектов убедитесь, что прикреплен только один экземпляр объекта с заданным значением ключа. ' - person Martin Robins; 08.08.2019
comment
да. Lookup.Key должен быть AlternateKey для поиска. Другой обходной путь - создать набор представлений для различных типов поиска. Или даже создать отдельные справочные таблицы от основной. - person David Browne - Microsoft; 08.08.2019
comment
Я пробовал использовать представления, но они не могут быть дочерним свойством сущности; хотя сущность может быть дочерним свойством представления! Lookup.Key не может быть альтернативным ключом, потому что он не уникален (если я чего-то не упускаю; я думал, что здесь могут помочь основные ключи). - person Martin Robins; 08.08.2019
comment
Это дает мне ту же ошибку с повторяющимися значениями ключей при сохранении PetType; LookupTypeID не устанавливается (для CustomerTitle и / или PetType) и всегда равен нулю. - person Martin Robins; 08.08.2019
comment
Вы бы никогда не изменили поиск в представлениях. Измените их с помощью основной таблицы поиска. Чтобы разрешить модификацию представлений, вам потребуются триггеры INSTEAD OF. - person David Browne - Microsoft; 08.08.2019