EF 4 Code First - Комбинированные виды и таблицы

Я исследовал этот вопрос в течение нескольких дней и, похоже, не могу найти вариант, который мне нравится; однако, здесь есть ссылка на очень похожий вопрос:

Добавить расчетное поле в модель

В конечном счете, у меня есть тот же вопрос, но я надеюсь на лучшее решение.

Рассмотрим следующие таблицы DB:

CREATE TABLE [Contact](
[ContactID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
[ContactName] [varchar](80) NOT NULL,
[Email] [varchar](80) NOT NULL,
[Title] [varchar](120) NOT NULL,
[Address1] [varchar](80) NOT NULL,
[Address2] [varchar](80) NOT NULL,
[City] [varchar](80) NOT NULL,
[State_Province] [varchar](50) NOT NULL,
[ZIP_PostalCode] [varchar](30) NOT NULL,
[Country] [varchar](50) NOT NULL,
[OfficePhone] [varchar](30) NOT NULL,
[MobilePhone] [varchar](30) NOT NULL)

CREATE TABLE [Blog](
[BlogID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
[BlogName] [varchar](80) NOT NULL,
    [CreatedByID] [int] NOT NULL,  -- FK to ContactTable
    [ModifiedByID] [int] NOT NULL  -- FK to ContactTable
)

CREATE TABLE [Post](
[PostID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
    [BlogID] [int] NOT NULL, -- FK to BlogTable
[Entry] [varchar](8000) NOT NULL,
    [CreatedByID] [int] NOT NULL,  -- FK to ContactTable
    [ModifiedByID] [int] NOT NULL  -- FK to ContactTable
)

Теперь я хотел бы использовать представления для загрузки "общего" поиска/расчета информации. Каждый раз, когда мы показываем сообщение на сайте, мы хотим узнать имя человека, создавшего сообщение, и кто его последний раз модифицировал. Это два поля, которые хранятся в отдельных таблицах из таблицы сообщений. Я мог бы легко использовать следующий синтаксис (предполагая, что была применена Lazy/eager loading, а BuiltBy - свойство типа Contact, на основе CreatedByID): currentPost.CreatedBy.Name;

Проблема с этим подходом - количество вызовов Db, а также большая запись, полученная для контакта, но мы используем только имя 99% в этой ситуации. Я понимаю, что схема DB выше крошечная, но это просто упрощенный пример, а реальная таблица контактов имеет около 50 полей.

Чтобы управлять этим типом ситуации в прошлом (до использования EF), я обычно создавал "подробные" представления для таблиц, которые я буду использовать. Представления "подробно" содержат общие поля поиска/расчета, так что для эффективного получения всей необходимой информации требуется только один вызов в БД (ПРИМЕЧАНИЕ. Мы также используем индексирование в наших представлениях SQL, чтобы сделать это чрезвычайно эффективным для чтения). список представлений, которые я обычно использую (поскольку они будут содержать поля "искать" из связанных таблиц):

ALTER VIEW [icoprod].[BlogDetail]
AS
SELECT  B.[BlogID], 
    B.[BlogName], 
    B.[BlogDescription],
    B.[CreatedByID], 
    B.[ModifiedByID],
    CREATEDBY.[ContactName] AS CreatedByName, 
    MODIFIEDBY.[ContactName] AS ModifiedByName,
    (SELECT COUNT(*) FROM Post P WHERE P.BlogID = B.BlogID) AS PostCount
FROM    Blog AS B 
JOIN Contact AS CREATEDBY ON B.CreatedByID = CREATEDBY.ContactID 
JOIN Contact AS MODIFIEDBY ON B.ModifiedByID = MODIFIEDBY.ContactID

ALTER VIEW [icoprod].[PostDetail]
AS
SELECT  P.[PostID], 
    P.[BlogID],
    P.[Entry], 
    P.[CreatedByID], 
    P.[ModifiedByID],
    CREATEDBY.[ContactName] AS CreatedByName, 
    MODIFIEDBY.[ContactName] AS ModifiedByName,
    B.Name AS BlogName
FROM    Post AS P
JOIN Contact AS CREATEDBY ON P.CreatedByID = CREATEDBY.ContactID 
JOIN Contact AS MODIFIEDBY ON P.ModifiedByID = MODIFIEDBY.ContactID
JOIN Blog AS B ON B.BlogID = P.BlogID

Вот обзор моих объектов "POCO":

public class Blog
{
    public int ID { get; set; }
    public string Name { get; set; }

    public int CreatedByID { get; set; }
    public DateTime ModifiedByID { get; set; }
}

public class Post
{
    public int ID { get; set; }
    public string Name { get; set; }

    public int CreatedByID { get; set; }
    public DateTime ModifiedByID { get; set; }
}

public class Contact
{
    public int ID { get; set; }
    public string Name { get; set; }

    public string Email { get; set; }
    public string Title { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string MobilePhone { get; set; }
}

public class BlogDetails : Blog
{
    public string CreatedByName { get; set; }
    public string ModifiedByName { get; set; }
    public int PostsCount { get; set; }
}

public class PostDetails : Post
{
    public string CreatedByName { get; set; }
    public string ModifiedByName { get; set; }
    public string BlogName { get; set; }
}

Причина, по которой мне нравится этот подход, заключается в том, что он позволяет мне извлекать информацию из базы данных на основе таблиц или представлений. И если я загружаю представление, представление содержит всю "таблицу", которая позволяет мне загружаться из представления а сохранить в таблицу. ИМО, это дает мне лучшее из обоих миров.

Я использовал этот подход в прошлом, но, как правило, я просто загружал информацию из БД, используя datarows или информацию из сохраненных процессов или даже используемую дозвуковую схему activerecord и отображаемые поля после загрузки из БД. Я действительно надеюсь, что смогу сделать что-то в EF, что позволяет мне загружать эти объекты, не создавая еще один слой абстракции.

Вот что я пытался использовать для настройки (используя Fluent API и EF с кодовым кодом):

public class PostConfiguration : EntityTypeConfiguration<Post>
{
    public PostConfiguration()
        : base()
    {
        HasKey(obj => obj.ID);

        Property(obj => obj.ID).
            HasColumnName("PostID").
            HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).
            IsRequired();

        Map(m =>
            {
                m.ToTable("Post");
            });
    }
}

public class BlogConfiguration : EntityTypeConfiguration<Blog>
{
    public BlogConfiguration()
        : base()
    {
        HasKey(obj => obj.ID);

        Property(obj => obj.ID).
            HasColumnName("BlogID").
            HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).
            IsRequired();

        Map(m =>
            {
                m.ToTable("Blog");
            });
    }
}

public class ContactConfiguration : EntityTypeConfiguration<Contact>
{
    public ContactConfiguration()
        : base()
    {
        HasKey(obj => obj.ID);

        Property(obj => obj.ID).
            HasColumnName("ContactID").
            HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).
            IsRequired();

        Map(m =>
            {
                m.ToTable("Contact");
            });
    }
}

public class PostDetailsConfiguration : EntityTypeConfiguration<PostDetails>
{

    public PostDetailsConfiguration()
        : base()
    {

        Map(m =>
            {
                m.MapInheritedProperties();
                m.ToTable("icoprod.PostDetails");
            });

    }

}

public class BlogDetailsConfiguration : EntityTypeConfiguration<BlogDetails>
{

    public BlogDetailsConfiguration()
        : base()
    {

        Map(m =>
            {
                m.MapInheritedProperties();  
                m.ToTable("icoprod.BlogDetails");
            });

    }

}

В этот момент я попытался использовать представление, содержащее всю информацию из таблицы с "расширенной" информацией, и когда я попробую это, я получаю страшную ошибку 3032 (пример ошибки здесь). Затем я попытался иметь представление ТОЛЬКО содержать главный ключ таблицы и "расширенные" свойства (например, [Entry] не находится в представлении PostDetails). Когда я пытаюсь это сделать, я получаю следующую ошибку:

All objects in the EntitySet 'DBContext.Post' must have unique primary keys. However, an instance of type 'PostDetails' and an instance of type 'Post' both have the same primary key value, 'EntitySet=Post;ID=1'.

Итак, я играл с оставлением MapInheritedProperties немного, но без везения. Я по-прежнему получаю аналогичную ошибку.

Есть ли у кого-нибудь предложение о том, как "расширить" объект base/table и загрузить информацию из представления? Опять же, я считаю, что при этом получается большая производительность. В статье, на которую я ссылался в начале этого вопроса, есть 2 потенциальных решения, но 1 требует слишком большого количества удалений БД (просто чтобы получить некоторую общую информацию о поиске), а другой требует дополнительного слоя абстракции (и мне бы очень хотелось перейти непосредственно к мой POCO из БД, не записывая никакого сопоставления).

Наконец, благодарю вас всем, кто отвечает на эти вопросы. Я приветствую всех, кто внес свой вклад в ответы на протяжении многих лет. Я думаю, что многие из нас разработчики принимают эту информацию как должное!!

Ответ 1

Загрузка записи из представления и сохранение ее в таблицу не будет работать с кодовым отображением. Объект блога всегда будет загружен из таблицы и сохранен в таблицу, а объект BlogDetail всегда будет загружен из представления и сохранен для просмотра - поэтому у вас должен быть обновляемый или вместо триггера для поддержки этого сценария. Если вы используете EDMX, вы также можете сопоставить собственную SQL/хранимую процедуру, выполняемую для вставки, обновления и удаления, чтобы принудительно сохранить в таблицу, но эта функция недоступна при сопоставлении кода. Во всяком случае, это не ваша самая большая проблема.

Вы можете использовать свое представление, и вы можете сопоставить его с классом, как вы это делали, но вы не должны отображать наследование. Причина в том, как работает наследование. Inheritance говорит, что объект является родительским или дочерним (который может выступать в роли родителя). Никогда не может быть записи базы данных, которая может быть как родительским (я имею в виду только родителем), либо дочерним. Это даже невозможно в .NET, потому что для поддержки этого сценария вам понадобятся два экземпляра - родительского типа и один из дочерних. Эти два экземпляра не эквивалентны, потому что чистый родитель не может быть передан ребенку (это не ребенок). И здесь самая большая проблема. Когда вы наследуете наследование, ключ должен быть уникальным во всей иерархии наследования. Таким образом, у вас никогда не может быть двух экземпляров (один для родительского и один для дочернего) с одним и тем же ключом.

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

Другим обходным решением является не сопоставление BlogDetail. В таком случае вы можете использовать свой код как есть и вместо использования представления создать простой многоразовый запрос с проекцией:

var blogDetails = from b in context.Blogs
                  where ... 
                  select new BlogDetail
                      {
                          Name = b.Name,
                          CreatedByID = b.CreatedByID,
                          ...
                          CreatedByName = b.CreatedBy.Name // You need navigation property
                          ...   
                      }; 

В обоих случаях, если вам нужно сохранить Blog, вы должны создать новый экземпляр и заполнить его с BlogDetail. После этого вы присоедините его к контексту, установите его в измененное состояние и сохраните изменения.