Incluir com FromSqlRaw e procedimento armazenado no EF Core 3.1

8

Então, aqui está o acordo - atualmente estou usando o EF Core 3.1 e digamos que tenho uma entidade:

public class Entity
{
    public int Id { get; set; }
    public int AnotherEntityId { get; set; }
    public virtual AnotherEntity AnotherEntity { get; set; }
}

Quando acesso a DbSet<Entity> Entitiesmaneira normal, incluo AnotherEntity como:

_context.Entities.Include(e => e.AnotherEntity)

e isso funciona. Por que não, certo? Então eu vou com:

_context.Entities.FromSqlRaw("SELECT * FROM Entities").Include(e => e.AnotherEntity)

e isso também funciona. Ambos me devolvem a mesma coleção de objetos unidos ao AnotherEntity. Então eu uso um procedimento armazenado que consiste na mesma consulta SELECT * FROM Entitiesdenominada spGetEntities:

_context.Entities.FromSqlRaw("spGetEntities")

adivinha? Isso também funciona. Dá-me a mesma saída, mas sem entrar no AnotherEntity, obviamente. No entanto, se eu tentar adicionar o Incluir assim:

_context.Entities.FromSqlRaw("spGetEntities").Include(e => e.AnotherEntity)

Estou obtendo:

FromSqlRaw ou FromSqlInterpolated foi chamado com SQL não-composable e com uma consulta compondo sobre ele. Considere chamar AsEnumerable após o método FromSqlRaw ou FromSqlInterpolated para executar a composição no lado do cliente.

Mesmo que a saída de _context.Entities.FromSqlRaw("SELECT * FROM Entities")e _context.Entities.FromSqlRaw("spGetEntities") seja idêntica.

Não consegui encontrar uma prova de que posso ou não posso fazer isso com o EF Core 3.1, mas se alguém pudesse me dar alguma sugestão de possibilidade dessa abordagem, seria bom.

Além disso, se houver outra maneira de associar entidades usando o procedimento armazenado, provavelmente o aceitaria como a solução do meu problema.

Gleb
fonte
2
Não é a EF que não pode fazer isso. É o próprio SQL ("SQL não compostável"), portanto, muito menos a EF poderia.
Gert Arnold
@GertArnold, adicione-o como resposta. Também ajudará outros usuários.
Lutti Coelho
_context.Something.FromSqlRaw ("EXECUTE dbo.spCreateSomething @Id, @Year", sqlParameters) .IgnoreQueryFilters (). AsNoTracking (). AsEnumerable (). FirstOrDefault (); Isso funciona para mim, você pode usar ToList () também .AsEnumerable (). FirstOrDefault () para obter muitos.
Chris Go

Respostas:

6

Em breve, você não pode fazer isso (pelo menos para SqlServer). A explicação está contida na documentação do EF Core - Consultas SQL não processadas - Compondo com LINQ :

A composição com LINQ exige que sua consulta SQL bruta seja compostável, pois o EF Core tratará o SQL fornecido como uma subconsulta. As consultas SQL que podem ser compostas começam com a SELECTpalavra - chave. Além disso, o SQL passado não deve conter caracteres ou opções que não sejam válidos em uma subconsulta, como:

  • Um ponto e vírgula à direita
  • No SQL Server, uma dica no nível da consulta à direita (por exemplo, OPTION (HASH JOIN))
  • No SQL Server, uma ORDER BYcláusula que não é usada OFFSET 0 OR TOP 100 PERCENTna SELECTcláusula

O SQL Server não permite a composição de chamadas de procedimento armazenado, portanto, qualquer tentativa de aplicar operadores de consulta adicionais a essa chamada resultará em SQL inválido. Use AsEnumerableou AsAsyncEnumerablemétodo logo após FromSqlRawou FromSqlInterpolatedmétodos para garantir que o EF Core não tente compor sobre um procedimento armazenado.

Além disso, uma vez que Include/ ThenIncluderequer o EF Core IQueryable<>, AsEnumerable/ AsAsyncEnumerableetc. não é uma opção. Você realmente precisa de SQL composível, portanto os procedimentos armazenados não são uma opção.

Porém, em vez de procedimentos armazenados, você pode usar TVF (Table-Valued Functions) ou exibições de banco de dados porque elas são composíveis ( select * from TVF(params)ou select * from db_view).

Ivan Stoev
fonte
Isso não funciona no meu caso, porque estou usando um tipo derivado. Ao usar o tipo derivado do tipo usado no modelo, a consulta será composta mesmo se você chamar AsEnumerable logo após FromSqlRaw. Não vejo outra solução senão tornar esse tipo não derivado, separe com todas as propriedades do tipo base, o que não é conveniente.
Hrvoje Batrnek
1
@HrvojeBatrnek Eu acho que você tem em mente stackoverflow.com/questions/61070935/…
Ivan Stoev
2

No meu caso, eu estava convertendo EF de trabalho FromSql()com um código de procedimento armazenado 2.1 para 3.1. Igual a:

ctx.Ledger_Accounts.FromSql("AccountSums @from, @until, @administrationId",
                                                            new SqlParameter("from", from),
                                                            new SqlParameter("until", until),
                                                            new SqlParameter("administrationId", administrationId));

Onde AccountSumsé um SP.

A única coisa que eu precisava fazer era usar FromSqlRaw()e adicionar IgnoreQueryFilters()para que funcionasse novamente. Igual a:

ctx.Ledger_Accounts.FromSqlRaw("AccountSums @from, @until, @administrationId",
               new SqlParameter("from", from),
               new SqlParameter("until", until),
               new SqlParameter("administrationId", administrationId)).IgnoreQueryFilters();

Isso é mencionado nos comentários, mas eu perdi isso a princípio, incluindo isso aqui.

Flores
fonte