首页 > 解决方案 > 如何在 Entity Framework Core 3.1 中对自定义对象执行原始 SQL 查询,而不需要创建表的迁移?

问题描述

我正在查询一个Store表以向用户显示 10 个最接近Store的 s。我想显示NameDistanceStore但更喜欢在自定义实体中保持距离。

Store字段:、、、、IdName字段:名称距离`LatitudeLongitude
StoreDtoId,,

这个 SO 答案让我们走上正轨,尤其是评论。但是,现在不推荐使用 DbQuery。

Keyless Entity Types上的文档说我们可以使用 Keyless Entity Type 作为原始 SQL 查询的返回类型。

我的 DbContext 已经有:

public DbSet<Store> Stores { get; set; }

添加

public DbSet<StoreDto> StoreDtos { get; set; }

modelBuilder.Entity<QuestSiteDto>()
    .HasNoKey()
    .ToView(null); // Hack to prevent table generation

允许我的商店搜索代码工作。但是下次我运行迁移时,EF Core 想要创建一个 StoreDto 表,除非我添加那个丑陋的ToView(null)hack。

作为参考,这是我的查询:

var sql = 
@"select 
    geography::Point({0}, {1}, 4326).STDistance(geography::Point(Latitude, Longitude, 4326)) / 1609.34 as Distance,
    Id,
    [Name]
from
    Store"

var results = await StoreDtos
    .FromSqlRaw(sql, latitudeUnsafe, longitudeUnsafe)
    .OrderBy(x => x.Distance)
    .Take(10)
    .ToListAsync();

这样做的正确方法是什么?如果你相信你知道推荐的方式,你能引用你的来源吗?截至本文发布之时,Keyless Entity Types 文档页面更多地关注视图和表,而不是原始查询(除非我遗漏了什么)。

标签: c#entity-frameworkasp.net-coreentity-framework-core

解决方案


您还可以查询未在 DbContext 中注册的类型。这个想法是为每个 ad-hoc 查询类型引入一个单独的单实体 DbContext 类型。每个都将被单独初始化和缓存。

所以只需添加一个这样的扩展方法:

   public static class SqlQueryExtensions
    {
        public static IList<T> SqlQuery<T>(this DbContext db, Func<T> targetType, string sql, params object[] parameters) where T : class
        {
            return SqlQuery<T>(db, sql, parameters);
        }
        public static IList<T> SqlQuery<T>(this DbContext db, string sql, params object[] parameters) where T : class
        {

            using (var db2 = new ContextForQueryType<T>(db.Database.GetDbConnection()))
            {
                return db2.Query<T>().FromSql(sql, parameters).ToList();
            }
        }


        class ContextForQueryType<T> : DbContext where T : class
        {
            DbConnection con;

            public ContextForQueryType(DbConnection con)
            {
                this.con = con;
            }
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                //switch on the connection type name to enable support multiple providers
                //var name = con.GetType().Name;

                optionsBuilder.UseSqlServer(con);

                base.OnConfiguring(optionsBuilder);
            }
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                var t = modelBuilder.Query<T>();

                //to support anonymous types, configure entity properties for read-only properties
                foreach (var prop in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public ))
                {
                    if (!prop.CustomAttributes.Any(a => a.AttributeType == typeof(NotMappedAttribute)))
                    {
                        t.Property(prop.Name);
                    }
                    
                }
                base.OnModelCreating(modelBuilder);
            }
        }

    }

或者对于 EF Core 5:

public static class SqlQueryExtensions
{
    public static IList<T> SqlQuery<T>(this DbContext db, Func<T> targetType, string sql, params object[] parameters) where T : class
    {
        return SqlQuery<T>(db, sql, parameters);
    }
    public static IList<T> SqlQuery<T>(this DbContext db, string sql, params object[] parameters) where T : class
    {

        using (var db2 = new ContextForQueryType<T>(db.Database.GetDbConnection()))
        {
            return db2.Set<T>().FromSqlRaw(sql, parameters).ToList();
        }
    }


    class ContextForQueryType<T> : DbContext where T : class
    {
        DbConnection con;

        public ContextForQueryType(DbConnection con)
        {
            this.con = con;
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            //switch on the connection type name to enable support multiple providers
            //var name = con.GetType().Name;

            optionsBuilder.UseSqlServer(con);

            base.OnConfiguring(optionsBuilder);
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            var t = modelBuilder.Entity<T>().HasNoKey();

            //to support anonymous types, configure entity properties for read-only properties
            foreach (var prop in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                if (!prop.CustomAttributes.Any(a => a.AttributeType == typeof(NotMappedAttribute)))
                {
                    t.Property(prop.Name);
                }

            }
            base.OnModelCreating(modelBuilder);
        }
    }

}

使用看起来像:

using (var db = new Db())
{
    var results = db.SqlQuery<ArbitraryType>("select 1 id, 'joe' name");
    //or with an anonymous type like this
    var results2 = db.SqlQuery(() => new { id =1, name=""},"select 1 id, 'joe' name");
}

这最初出现在这里,但 github 问题评论线程不是很容易发现:https ://github.com/dotnet/efcore/issues/1862#issuecomment-451671168


推荐阅读