entity framework 实现按照距离排序
在做项目时,经常会遇到“离我最近”这种需求。顾名思义,它需要根据用户的经纬度和事物的经纬度计算距离,然后进行排序,最后分页(当然这些操作要在数据库中进行,否则就变成假分页了)。
我们通常可以用sql语句来实现
SELECT es_name, es_lon, es_lat, ROUND( 6378.138 * 2 * ASIN( SQRT( POW( SIN( ( 30.611842 * PI() / 180 - es_lat * PI() / 180 ) / 2 ), 2 ) + COS(30.611842 * PI() / 180) * COS(es_lat * PI() / 180) * POW( SIN( ( 104.074666 * PI() / 180 - es_lon * PI() / 180 ) / 2 ), 2 ) ) ) * 1000 ) AS distance_um FROM c_ershuai ORDER BY distance_um ASC
但是我比较习惯使用 entity framework,于是我就想着能不能用 entity framework 实现按照距离排序。
以下是我采用的方案
首先定义一个接口,用来表示具有经纬度信息的实体。
/// <summary> /// 具有经纬度 /// </summary> public interface IHasLngAndLat { /// <summary> /// 经度 /// </summary> double Lng { get; set; } /// <summary> /// 纬度 /// </summary> double Lat { get; set; } }
然后创建泛型类,用来包装计算的距离。
/// <summary> /// 带距离的数据 /// </summary> /// <typeparam name="TEntity"></typeparam> public class DataWithDistance<TEntity> { /// <summary> /// 距离(km) /// </summary> public double Distance { get; set; } /// <summary> /// 实体数据 /// </summary> public TEntity Entity { get; set; } }
最后编写根据距离排序的扩展方法
注意:这个方法是采用的 SqlFunctions 类,所以仅支持SqlServer数据库,如果是其它数据库,需要将 SqlFunctions 更换成对应的类
/// <summary> /// IQueryable扩展类 /// </summary> public static class QueryableExtension { /// <summary> /// 根据距离排序 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="queryable"></param> /// <param name="lng">经度</param> /// <param name="lat">纬度</param> /// <returns></returns> public static IQueryable<DataWithDistance<TEntity>> OrderByDistance<TEntity>(this IQueryable<TEntity> queryable, double lng, double lat) where TEntity : class, IHasLngAndLat { var rtn = from q in queryable let radLat1 = lat * Math.PI / 180.0 let radLat2 = q.Lat * Math.PI / 180.0 let a = radLat1 - radLat2 let b = lng * Math.PI / 180.0 - q.Lng * Math.PI / 180.0 let s = 2 * SqlFunctions.Asin(SqlFunctions.SquareRoot(Math.Pow((double)SqlFunctions.Sin(a / 2), 2) + SqlFunctions.Cos(radLat1) * SqlFunctions.Cos(radLat2) * Math.Pow((double)SqlFunctions.Sin(b / 2), 2))) * 6378.137 let d = Math.Round((double)s * 10000) / 10000 orderby d select new DataWithDistance<TEntity> { Entity = q, Distance = d }; return rtn; } }
以上就完成了 entity framework 按照距离排序的功能。
接下来我们用它来写一个小小的demo
首先创建一个商店实体类,具有经纬度字段,实现了 IHasLngAndLat 接口。
/// <summary> /// 商店实体 /// </summary> public class Shop : IHasLngAndLat { /// <summary> /// 主键 /// </summary> public int Id { get; set; } /// <summary> /// 商店名称 /// </summary> [Required] [StringLength(64)] public string ShopName { get; set; } /// <summary> /// 经度 /// </summary> public double Lng { get; set; } /// <summary> /// 纬度 /// </summary> public double Lat { get; set; } }
然后创建EF上下文类
/// <summary> /// EF上下文 /// </summary> public class DemoDbContext : DbContext { public DemoDbContext() : base("name=DemoDbContext") { } public virtual DbSet<Shop> Shop { get; set; } }
最后我们分页查询商店,并按照距离由近到远排序
#region 入参 double user_lng = 113.46, user_lat = 22.27; //用户经纬度 int pageIndex = 3; //当前页码 int pageSize = 10; //每页条数 #endregion using (DemoDbContext context = new DemoDbContext()) { var queryable = context.Shop.AsNoTracking().AsQueryable(); IQueryable<DataWithDistance<Shop>> sort_queryable = queryable.OrderByDistance(user_lng, user_lat); //按照用户的距离从近到远排序 List<DataWithDistance<Shop>> data = sort_queryable.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); //分页并执行sql查询获取数据 //TODO:将查到的数据映射成DTO对象,并返回给客户端 }
好了,entity framework 实现按照距离排序 也就全部完成了。