如何使用EF Core执行原始SQL查询?

4

我的问题基本上是我想在一个不是我的模型表示的数据库中运行查询。

这是连接到另一个数据库的代码:

public static OtherContext GetNewContextGeneric(string connectionString)
        {
            var builder = new DbContextOptionsBuilder();
            builder.UseSqlServer(connectionString);

            OtherContext db = new OtherContext(builder.Options);

            return db;
        }

这是我用来执行查询的代码:

public List<IQueryble> Query (string connectionString, string query)
        {
            try
            {
                using(var contextGeneric = ContextFactory.GetNewContextGeneric(connectionString))
                {
                    //I want something like this
                    return contextGeneric.Query(query).ToList();
                }
            }
            catch(System.Data.SqlClient.SqlException ex)
            {
                throw new SQLIncorrectException(ex);
            }
            catch(System.InvalidOperationException ex)
            {
                throw new NotImplementedException();
            }   
        }

有人可以帮我吗?


抱歉,是的,对此感到抱歉。 - Daniel Acevedo
你想运行什么查询?你是否能够获取记录?还是更多的是一次性执行而不需要返回结果? - Erik Philips
为了处理更复杂的查询,为什么不创建一个代表你要返回内容的模型呢?你可以将结果简单地投射到该模型中,而该模型不需要是任何实体框架模型。 - Erik Philips
1
可能是Raw SQL Query without DbSet - Entity Framework Core的重复问题。 - ErikEJ
这是一个非常相似的副本,不同之处在于此帖子不是关于扩展现有上下文,而是查询外部数据库,应使用与dup相同的概念来解决此问题,只需进行轻微更改以支持通过连接字符串传递而不是DbConnection - Chris Schaller
显示剩余5条评论
5个回答

3
您可以使用 DbDataReader:
using (var command = context.Database.GetDbConnection().CreateCommand())
{
    command.CommandText = "SELECT * From Make";
    context.Database.OpenConnection();
    using (var reader = command.ExecuteReader())
    {
        // Do something with result
        reader.Read(); // Read first row
        var firstColumnObject = reader.GetValue(0);
        var secondColumnObject = reader.GetValue(1);

        reader.Read(); // Read second row
        firstColumnObject = reader.GetValue(0);
        secondColumnObject = reader.GetValue(1);
    }
}

在这里,你可以学习如何从 DbDataReader 中读取值。

或者你可以使用 FromSql() 方法,但它只能在某个实体的预定义 DbSet 上工作,这不是你想要的解决方案。


我遇到了一个错误,提示我在执行ExecuteReader时连接已关闭。 - Daniel Acevedo

2

您可以使用EF Core来实现对外部数据库的使用案例,无需使用ADO.Net,这个解决方案基于@ErikEj提供的针对EF Core的通用解决方案。(请注意,一些函数和命名空间在EF Core 3中已更改,但仍保留在.NET 5+中,但是相同的通用概念仍然适用)

public static IList<T> Query<T>(string connectionString, string query, params object[] parameters) where T : class
{
    try
    {
        using (var contextGeneric = new ContextForQuery<T>(connectionString))
        {
            return contextGeneric.Query<T>().FromSql(query, parameters).ToList();
        }
    }
    catch (System.Data.SqlClient.SqlException ex)
    {
        throw new SQLIncorrectException(ex);
    }
    catch (System.InvalidOperationException ex)
    {
        throw new NotImplementedException();
    }
}

private class ContextForQuery<T> : DbContext where T : class
{
    private readonly string connectionString;

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

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(connectionString, options => options.EnableRetryOnFailure());

        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<T>().HasNoKey();
        base.OnModelCreating(modelBuilder);
    }
}

因此,使用它需要具体类型定义的支持,为匿名类型添加支持需要更多的努力,但为此创建具体类型并不是坏事,这里的整个重点是尝试让您更加倾向于声明性代码风格,因为它们提高了代码的可读性和检查性,并提供了文档和其他扩展配置,如相关实体。

public class NamedObject
{
    public int Id { get; set; }
    public string Name { get; set; }
}

...

var connectionString = "Insert your connection string here...";
var data = Query<NamedObject>(connectionString, "SELECT TOP 10 Id, FullName as Name FROM Employee");
foreach (var emp in data)
{
    Console.WriteLine(emp.Name);
}

背景

在EF 6(.Net Framework)中,我们可以使用DbContext.Database.FromSQL<T>()执行即席SQL语句,自动映射到指定类型的T。由于FromSQL的结果与EF的其余部分不一致,结果是单个IEnumerable<T>,因此在EF Core中没有复制此功能。您无法进一步组合此查询以Include()相关实体,也无法将过滤器添加到基础查询中。

在 EF Core 中,要执行原始 SQL 语句并返回类型 T,需要在 DbContext 中将其定义为 DbSet<T>。实际上,这个集合不需要映射到数据库中的任何表格,事实上,自从 EF Core 2.1 开始,我们甚至不需要为此类型指定一个。它只是一种预定义期望结构的机制,而不是按需执行 Ad-Hoc 请求,它为您提供了与传统的 FromSQL 相同的功能,但还允许您定义丰富的导航属性集,以便在将您的 RawSQL 插值到 LINQ to SQL 管道后进一步组合查询。
一旦在上下文中定义了类型,只需调用 DbSet<T>.FromSqlRaw()。不同之处在于,现在我们有了一个IQueryable<T>,我们可以使用它来进一步compose以包含相关实体或应用在数据库内进行评估的筛选器。
此响应中发布的解决方案不允许组合,而是按预期的序列使用 EF 运行时来提供与原始 EF 6 实现相同的行为。
在最新版本的 EF Core 中,并且现在在 .Net 5+ 中需要应用以下微妙的更改:
  • Core 2.1: return contextGeneric.Query<T>().FromSql(query, parameters).ToList();
  • Core 3+: return contextGeneric.Set<T>().FromSqlRaw(query, parameters).ToList();

1
仅有一个非常小的评论:如果您将扩展方法Query<T>添加到类中,则私有类(ContextForQuery<T>)也必须在同一类中。在 .net 中,私有类不允许作为命名空间中的顶级类。顺便说一句,我喜欢这个解决方案。谢谢。 - MPowerGuy
完全正确,@MPowerGuy。当时我没有想到要指出这一点,但你是对的。你会时不时地看到像这样的私有类实现,在这种情况下,我创建了一个非常特殊的使用DbContext实现,我希望保留替换或修改EF未来版本的权利。如果将该类公开,很有可能它会成为其他逻辑的依赖项,而我不想负责后期维护。 - Chris Schaller
对于那些关注过我的其他回复的人,这是我个人解决方案的一个变体,用来反驳任何试图驳斥我所提出的"EF比Dapper更好"这一前提的人。 - Chris Schaller
我仍在努力理解一些泛型语法,所以如果问题太蠢,请原谅:扩展方法IList<T> Query<T>的签名是否应该将以下内容作为其第一个参数:this DbContext db,?就像这样: public static IList<T> Query<T>(this DbContext db, string connectionString, string query, params object[] parameters) where T : class ... 还是其他什么?我似乎找不到Query方法。 - MPowerGuy
啊,不用在意。我笨了,这个方法只是挂在我的扩展类上面而已。抱歉。 - MPowerGuy

1
在你的问题中,你说:
基本上我的问题是,我想在一个不代表我的模型的数据库中运行查询。
然后在评论中你补充道:
因为我不知道数据库是如何创建的,也不知道要在哪些表中插入SQL查询语句。
如果你不知道数据库,那么你就不能使用Entity Framework,因为它需要你对连接的数据库有详细的了解。你应该使用普通的ADO.NET(或者Dapper,如果你想将结果映射回已知的类)。

1
这是一个非常误导性的回答。实际上,EF并不需要比ADO或Dapper更多的知识,事实上,如果你从数据库生成EF模型,EF需要的知识反而更少,因为整个架构可以用具体的类定义来表示,这将使得在编写代码和查询时更容易使用智能提示进行探索和操作。 - Chris Schaller
@ChrisSchaller EF怎么可能需要更少的知识呢?原帖说他们无法访问数据库以检查有哪些表,那他们怎么能够从数据库创建模型呢?多读几遍我的回答,你就会明白了。此外,看看原帖作者的回答,就会发现他们完全忽略了EF。 - Camilo Terevinto
1
使用EF,您只需要一个连接字符串,然后就可以从数据库中生成模型。我说“更少的”知识,因为现在有了完全类型化的C#模型,您可以使用LINQ安全地查询和探索数据库,而无需编写单个SQL语句。如果没有EF(正如OP所做的那样),要做到这一点需要您首先“了解”模式,否则您怎么知道要在SQL语句中编写什么。EF可以为您提供该信息,将问题域从SQL转移到C#对象图中。 - Chris Schaller

1

您可以使用 context.Database.ExecuteSqlRaw("select 1")

不要忘记导入正确的命名空间:using Microsoft.EntityFrameworkCore;


1
看起来 OP 想要返回一个列表。我不认为在 EF Core 中使用 ExecuteSqlRaw 可以实现,是吗? - MPowerGuy
你可以使用输出参数:https://stackoverflow.com/questions/60382040/how-to-utilize-output-parameters-in-ef-core-3-0-using-executesqlinterpolatedasyn - undefined

-3

它的工作原理是这样的:

private void SqlCommand (string connectionString, string query)
        {
            using (SqlConnection connection = new SqlConnection(connectionString))
            {
                SqlCommand command = new SqlCommand(query, connection);
                connection.Open();
                SqlDataReader reader = command.ExecuteReader();
                try
                {
                    while (reader.Read())
                    {
                        var a = reader[0];
                    }
                }
                finally
                {
                    // Always call Close when done reading.
                    reader.Close();
                }
            }
        }

或者

using (var connection = ContextFactory.GetNewContextGeneric(connectionString).Database.GetDbConnection())
                {
                    connection.Open();
                    DbCommand command = connection.CreateCommand();
                    command.CommandText = query;

                    using (var reader = command.ExecuteReader())
                    {
                        // Do something with result
                        reader.Read(); // Read first row
                        var firstColumnObject = reader.GetValue(0);
                        /*var secondColumnObject = reader.GetValue(1);

                        reader.Read(); // Read second row
                        firstColumnObject = reader.GetValue(0);
                        secondColumnObject = reader.GetValue(1);*/
                        connection.Close();
                        return firstColumnObject.ToString();
                    }
                }

你几乎没有使用EF Core,只是获取连接并使用原始的ado.net。 - Chris Schaller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接