LINQ генерирует неправильный SQL (ссылка на несуществующую таблицу) - PullRequest
1 голос
/ 01 февраля 2012

Проект MVC3, использующий LINQ to Entity и Entity Framework 4. Code-First.

В другом посте ( Возврат товаров, принадлежащих всем тегам в списке, используя LINQ ), я получил помощь в создании оператора LINQ для возврата подмножества данных.

LINQ синтаксически корректен и компилируется, но генерирует неправильный SQL. В частности, он ссылается на несуществующую таблицу. Если я исправлю имя таблицы, он вернет правильные данные, поэтому LINQ кажется правильным.

Обратите внимание, что в целях сохранения длинного поста я не буду публиковать классы объектов (Product, Tag и ProductTag), но они перечислены в моем предыдущем вопросе: Возврат продуктов, принадлежащих все теги в списке, используя LINQ

LINQ:

var tags = "administration+commerce"
var tagParams = tags.Split('+').ToList();   //used in linq statement below

_repository.Products.Where(p => tagParams.All(tag => p.Tags.Select(x => x.Name).Contains(tag))).Distinct().Take(75).ToList();   


Ниже приводится неправильный и правильный код SQL.

Неверный SQL делает ссылки на несуществующую таблицу

[dbo].[TagProduct] 

а также искаженное поле

[ExtentN].[Tag_TagId]

Если я исправлю их в «[dbo]. [ProductTag]» и «[ExtentN]. [TagId]», SQL будет выполнен правильно и вернет правильные данные.

Сгенерированный LINQ (и неисправный) SQL

SELECT 
[Extent1].[ProductId] AS [ProductId], 
[Extent1].[Name] AS [Name], 
[Extent1].[ShortDescription] AS [ShortDescription], 
[Extent1].[LongDescription] AS [LongDescription], 
[Extent1].[Price] AS [Price]
FROM [dbo].[Product] AS [Extent1]
WHERE  NOT EXISTS (SELECT 
    1 AS [C1]
    FROM  (SELECT 
        N'administration' AS [C1]
        FROM  ( SELECT 1 AS X ) AS [SingleRowTable1]
    UNION ALL
        SELECT 
        N'commerce' AS [C1]
        FROM  ( SELECT 1 AS X ) AS [SingleRowTable2]) AS [UnionAll1]
    WHERE ( NOT EXISTS (SELECT 
        1 AS [C1]
        FROM  [dbo].[TagProduct] AS [Extent2]
        INNER JOIN [dbo].[Tag] AS [Extent3] ON [Extent3].[TagId] = [Extent2].[Tag_TagId]
        WHERE ([Extent1].[ProductId] = [Extent2].[Product_ProductId]) AND ([Extent3].[Name] = [UnionAll1].[C1])
    )) OR (CASE WHEN ( EXISTS (SELECT 
        1 AS [C1]
        FROM  [dbo].[TagProduct] AS [Extent4]
        INNER JOIN [dbo].[Tag] AS [Extent5] ON [Extent5].[TagId] = [Extent4].[Tag_TagId]
        WHERE ([Extent1].[ProductId] = [Extent4].[Product_ProductId]) AND ([Extent5].[Name] = [UnionAll1].[C1])
    )) THEN cast(1 as bit) WHEN ( NOT EXISTS (SELECT 
        1 AS [C1]
        FROM  [dbo].[TagProduct] AS [Extent6]
        INNER JOIN [dbo].[Tag] AS [Extent7] ON [Extent7].[TagId] = [Extent6].[Tag_TagId]
        WHERE ([Extent1].[ProductId] = [Extent6].[Product_ProductId]) AND ([Extent7].[Name] = [UnionAll1].[C1])
    )) THEN cast(0 as bit) END IS NULL)
)

Исправленный SQL

SELECT 
[Extent1].[ProductId] AS [ProductId], 
[Extent1].[Name] AS [Name], 
[Extent1].[ShortDescription] AS [ShortDescription], 
[Extent1].[LongDescription] AS [LongDescription], 
[Extent1].[Price] AS [Price]
FROM [dbo].[Product] AS [Extent1]
WHERE  NOT EXISTS (SELECT 
    1 AS [C1]
    FROM  (SELECT 
        N'administration' AS [C1]
        FROM  ( SELECT 1 AS X ) AS [SingleRowTable1]
    UNION ALL
        SELECT 
        N'commerce' AS [C1]
        FROM  ( SELECT 1 AS X ) AS [SingleRowTable2]) AS [UnionAll1]
    WHERE ( NOT EXISTS (SELECT 
        1 AS [C1]
        FROM  [dbo].[ProductTag] AS [Extent2]
        INNER JOIN [dbo].[Tag] AS [Extent3] ON [Extent3].[TagId] = [Extent2].[TagId]
        WHERE ([Extent1].[ProductId] = [Extent2].[ProductId]) AND ([Extent3].[Name] = [UnionAll1].[C1])
    )) OR (CASE WHEN ( EXISTS (SELECT 
        1 AS [C1]
        FROM  [dbo].[ProductTag] AS [Extent4]
        INNER JOIN [dbo].[Tag] AS [Extent5] ON [Extent5].[TagId] = [Extent4].[TagId]
        WHERE ([Extent1].[ProductId] = [Extent4].[ProductId]) AND ([Extent5].[Name] = [UnionAll1].[C1])
    )) THEN cast(1 as bit) WHEN ( NOT EXISTS (SELECT 
        1 AS [C1]
        FROM  [dbo].[ProductTag] AS [Extent6]
        INNER JOIN [dbo].[Tag] AS [Extent7] ON [Extent7].[TagId] = [Extent6].[TagId]
        WHERE ([Extent1].[ProductId] = [Extent6].[ProductId]) AND ([Extent7].[Name] = [UnionAll1].[C1])
    )) THEN cast(0 as bit) END IS NULL)
)


Опять же, единственные изменения в SQL это

[dbo].[TagProduct] changed to [dbo].[ProductTag]
[ExtentN].[Tag_TagId] changed to [ExtentN].[TagId]

Примечание. Я убедился, что в базе данных нет объекта с именем dbo.TagProduct, и в моем коде нет ссылок на TagProduct (и никогда не было).

Есть ли проблема в моем операторе LINQ, или это ошибка LINQ? У меня все в порядке, и я просто удалил хранимую процедуру, но я бы лучше нашел решение.

Спасибо и извинения за длинный пост.

EDIT

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

Новая модель выглядит следующим образом:

public class Product
{
    .
    . 
    //public virtual List<Tag> Tags { get; set; }             // <--removed
    public virtual List<ProductTag> ProductTags { get; set; }
}

public class ProductTag
{
    .
    . 
    public virtual Product Product { get; set; }
    public virtual Tag Tag { get; set; }
}

public class Tag
{
    .
    . 
    //public virtual List<Product> Products { get; set; }      // <--removed
    public virtual List<ProductTag> ProductTags { get; set; }

}

1 Ответ

4 голосов
/ 01 февраля 2012

Если у вас нет дополнительного сопоставления в Fluent API в модели в вашей связанной публикации, сгенерированный SQL верен и ожидается.Почему?

Чтобы было понятно, я копирую вашу модель с соответствующими навигационными свойствами и помечаю их вместе:

public class Tag
{
    public int TagId { get; set; }

    public virtual List<Product> Products { get; set; }         /* 1 */
    public virtual List<ProductTag> ProductTags { get; set; }   /* 2 */
}

public class Product
{
    public int ProductId { get; set; }

    public virtual List<Tag> Tags { get; set; }                 /* 1 */
}

public class ProductTag
{
    public int ProductTagId { get; set; }

    public int ProductId { get; set; }
    public int TagId { get; set; }

    public virtual Product Product { get; set; }                /* 3 */
    public virtual Tag Tag { get; set; }                        /* 2 */
}

Итак, у вас есть многие ко многим отношение (/* 1 */) между Tag и Product, отношение один-ко-многим (/* 2 */) между Tag и ProductTag и одно-отношение ко многим (/* 3 */) между Product и ProductTag, когда свойство навигации в Product не отображается.

Поскольку у вас нет отображения для множестваВзаимосвязь «многие-многие» в Fluent API Entity Framework предполагает наличие таблиц базы данных, соответствующих соглашениям о сопоставлении, а именно:

  • Таблица объединения «многие-ко-многим», называемая ProductTags TagProducts.Если вы отключили множественное число, оно будет ожидать ProductTag или TagProduct.Я говорю « или », потому что имя зависит от таких факторов, как порядок наборов в вашем производном контексте и, возможно, даже порядок свойств навигации в ваших классах и т. Д. Таким образом, трудно предсказать имя всложная модель - в основном причина, по которой рекомендуется всегда явно определять отношения «многие ко многим» в Fluent API.

  • Один ключевой столбец в таблице с именем EntityClassName_EntityKeyName ->Tag_TagId

  • Другой ключевой столбец в таблице с Product_ProductId

В вашем запросе используется только это отношение «многие ко многим» (вы используете толькоProduct.Tags как единственное свойство навигации в запросе).Итак, EF создаст SQL-запрос, который включает в себя таблицу соединений (в вашем случае это TagProduct, но, как сказано, только случайно) и имена ключевых столбцов таблицы соединений, которые равны Tag_TagId и Product_ProductId.

Вы можете определить отображение «многие ко многим» в Fluent API следующим образом:

modelBuilder.Entity<Product>()
    .HasMany(p => p.Tags)
    .WithMany(t => t.Products)
    .Map(x =>
    {
        x.MapLeftKey("ProductId");
        x.MapRightKey("TagId");
        x.ToTable("ProductTag");
    });

Это создаст проблемы, хотя у вас уже есть объект ProductTag, который, очевидно, уже имеетсоответствующая таблица ProductTag.Это не может быть таблицей соединения для ваших отношений «многие ко многим» одновременно.Таблица соединений должна иметь другое имя, например x.ToTable("ProductTagJoinTable").

Мне интересно, действительно ли вы хотите эти три упомянутых отношения.Или почему вы ожидаете, что имя таблицы ProductTag принадлежит сущности ProductTag?Эта таблица и сущность вообще не включены в ваш запрос.

Изменить

Предложение изменить вашу модель: ваша сущность ProductTag не содержит никаких дополнительныхполя, кроме полей, необходимых для таблицы соединения многих ко многим.Поэтому я бы обозначил это как чистое отношение многих ко многим.Это означает:

  • Удалить класс сущности ProductTag из вашей модели
  • Удалить свойство навигации ProductTags из вашего Tag класса
  • Определить отображениев Fluent API, как показано выше (соответствует таблице соединений с именем ProductTag с двумя столбцами ProductId и TagId, которые образуют составной первичный ключ и являются внешними ключами для таблиц Product и Tag соответственно)

В результате у вас будет только одно отношение (многие-ко-многим между Product и Tag), а не три отношения, и я ожидаю, что ваш запрос будет работать.

...