[英]Entity Framework Core saving geography::Point throws "The supplied value is not a valid instance of data type geography"
[英]Entity Framework Core: Type Udt is not supported on this platform. (Spatial Data - Geography)
我正在试验实体框架核心,偶然发现了一个我以前从未见过的错误,无法弄清楚如何修复它。 我正在使用 .net Core Web API 2.0 和 EntityFramework Core 2.00-preview2-final
这是一个触发错误的简单示例。
(概念:从数据库中获取用户的简单端点)
错误:System.PlatformNotSupportedException:此平台不支持 Udt 类型。
有什么建议吗?
问题是我在我的数据库中使用 geography 但我在我的模型中将它用作字符串,因为实体框架核心还不支持空间数据......
有什么方法可以在不摆脱地理因素的情况下保持这个蛋糕的美味,因为它是一个重要的特征?
编辑:请参阅我对当前解决方案的回答
好的,这是我解决它的方法:
目的是在 Entity Framework Core 中保留地理(不使用 DbGeography)
1)我创建了一个名为 Location 的结构:
public struct Location
{
public double Longitude { get; set; }
public double Latitude { get; set; }
}
2) 将它添加到您的 EF 实体模型
public class User
{
public Location Location { get; set; }
}
3)将其隐藏在您的模型构建器中
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Ignore(x => x.Location);
}
4)生成迁移(Add-Migration migrationname)
5) 转到您的迁移文件 1231randomnumbers1231_migrationname.cs 并添加以下内容(这样我们创建另一列名为 Location 的 geography 类型),然后更新您的数据库(update-database):
migrationBuilder.Sql(@"ALTER TABLE [dbo].[User] ADD [Location] geography NULL");
6)(可选)我创建了一个静态类来更新数据库,如果您在多个表中有一个位置列,这很方便。
public static class GeneralDB
{
public static async Task UpdateLocation(DbContext ctx, string table, Location location, int id)
{
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
string query = String.Format(@"UPDATE [dbo].[{0}] SET Location = geography::STPointFromText('POINT(' + CAST({1} AS VARCHAR(20)) + ' ' + CAST({2} AS VARCHAR(20)) + ')', 4326) WHERE(ID = {3})"
, table.ToLower(), location.Longitude, location.Latitude, id);
await ctx.Database.ExecuteSqlCommandAsync(query);
}
public static async Task<Location> GetLocation(DbContext ctx, string table, int id)
{
Location location = new Location();
using (var command = ctx.Database.GetDbConnection().CreateCommand())
{
string query = String.Format("SELECT Location.Lat AS Latitude, Location.Long AS Longitude FROM [dbo].[{0}] WHERE Id = {1}"
, table, id);
command.CommandText = query;
ctx.Database.OpenConnection();
using (var result = command.ExecuteReader())
{
if (result.HasRows)
{
while (await result.ReadAsync())
{
location.Latitude = result.GetDouble(0);
location.Longitude = result.GetDouble(1);
}
}
}
}
return location;
}
}
这只适用于 EF Core 2.0
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
对于 EF Core 1.0,您必须找到一种替代方法来将“,”替换为“.”。 一个很好的旧时尚 .Replace() 方法可以完成这项工作。
location.Longitude.ToString().Replace(',', '.')
7) CRUD 示例:
7.1:阅读
public async Task<User> GetByIdAsync(int id)
{
User user = await ctx.User.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id);
user.Location = await GeneralDB.GetLocation(ctx, "user", id);
return user;
}
7.2:创建
public async Task<User> CreateAsync(User entity)
{
ctx.User.Add(entity);
await ctx.SaveChangesAsync();
await GeneralDB.UpdateLocation(ctx, "user", entity.Location, entity.Id);
return entity;
}
7.3:更新
public async Task<User> UpdateAsync(User entity)
{
ctx.User.Attach(entity);
ctx.Entry<User>(entity).State = EntityState.Modified;
await ctx.SaveChangesAsync();
await GeneralDB.UpdateLocation(ctx, "user", entity.Location, entity.Id);
return entity;
}
更新:从 EF Core 2.2 开始支持空间数据!:
https://docs.microsoft.com/en-us/ef/core/modeling/spatial
——
Eli,tnx 为您提供解决方案。 对我来说,这几乎是完美的解决方案。 我有两个问题:
问题
解决方案
create trigger VisitLocation_trigger on Visit
after UPDATE, INSERT, DELETE
as
if exists(SELECT * from inserted)
If exists(Select * from deleted)
BEGIN
-- UPDATE
UPDATE visit_location SET location = GEOGRAPHY::Point(Latitude, Longitude, 4326) FROM visit_location JOIN inserted ON visit_location.visitid = inserted.id
END
else
BEGIN
-- INSERT
INSERT INTO visit_location SELECT Id, GEOGRAPHY::Point(Latitude, Longitude, 4326) FROM inserted
END
else
BEGIN
-- DELETE
declare @visitId int;
SELECT @visitId = Id from deleted i;
DELETE visit_location WHERE visit_location.visitid = @visitId
end
_context.Visit.FromSql(
"SELECT TOP 50 v.* " +
"FROM visit v " +
"INNER JOIN visit_location vl ON v.id = vl.visitid " +
"WHERE v.date > {0} " +
"AND GEOGRAPHY::Point({1},{2}, 4326).STDistance(Location) < {3} " +
"ORDER BY GEOGRAPHY::Point({1},{2}, 4326).STDistance(Location)",
startDate, latitude, longitude, radius).ToList();
CRUD
读
public async Task<Visit> GetByIdAsync(int id)
{
return await _context.Visit.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id);
}
public IList<Visit> GetLastVisitsForHouseIdsByCoordinates(DateTime startDate, double longitude, double latitude, long radius)
{
return
_context.Visit.FromSql("SELECT TOP 50 v.* " +
"FROM visit v " +
"INNER JOIN visit_location vl ON v.id = vl.visitid " +
"WHERE v.IsLastVisit = 1 " +
"AND v.date > {0} " +
"AND GEOGRAPHY::Point({1},{2}, 4326).STDistance(Location) < {3} " +
"ORDER BY GEOGRAPHY::Point({1},{2}, 4326).STDistance(Location)",
startDate, latitude, longitude, radius).ToList();
}
创建
public async Task<Visit> CreateAsync(Visit visit)
{
_context.Visit.Add(visit);
await _context.SaveChangesAsync();
return visit;
}
更新
public async Task<Visit> UpdateAsync(Visit visit)
{
_context.Visit.Attach(visit);
_context.Entry(visit).State = EntityState.Modified;
await _context.SaveChangesAsync();
return visit;
}
删除
public async Task DeleteAsync(Visit visit)
{
_dbContext.Remove(entityToUpdate);
_context.Entry(visit).State = EntityState.Deleted;
await _context.SaveChangesAsync();
return visit;
}
数据库模型
public class Visit
{
public int Id { get; set; }
[Required]
public VisitStatus Status { get; set; }
[Required]
public double? Latitude { get; set; }
[Required]
public double? Longitude { get; set; }
public Location Location { get; set; }
[Required]
public DateTime Date { get; set; }
public string Street { get; set; }
public int? StreetNumber { get; set; }
public string StreetNumberLetter { get; set; }
public string StreetNumberLetterAddition { get; set; }
public string City { get; set; }
}
public struct Location
{
public double Longitude { get; set; }
public double Latitude { get; set; }
}
这些解决方案有效,但如果您正在寻找其他方法,这里是另一种解决方案。 由于 EF 核心 2 目前不支持地理类型,您可以使用 NetTopologySuite 来支持所有服务器端地理类型。
当您有一个需要 geography 列的表时,添加 EF 可以映射到您的表的属性,该表的类型为 byte[] 或 string。 像这样:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NetTopologySuite;
using NetTopologySuite.Geometries;
namespace GeoTest2.Data
{
public class HomeTown
{
private byte[] _pointsWKB;
public string Name { get; set; }
public byte[] PointWkb
{
get => _pointsWKB;
set
{
if (GeopgraphyFactory.CreatePoint(value) != null)
_pointsWKB = value;
throw new NotImplementedException("How ever you wnat to handle it");
}
}
[NotMapped]
public Point Point
{
get => GeopgraphyFactory.CreatePoint(_pointsWKB);
set => _pointsWKB = GeopgraphyFactory.PointAsWkb(value);
}
}
}
这使用了一些助手来创建点在这里:
using NetTopologySuite.Geometries;
namespace GeoTest2.Data
{
public static class GeopgraphyFactory
{
public static Point CreatePoint(byte[] wkb)
{
var reader = new NetTopologySuite.IO.WKBReader();
var val = reader.Read(wkb);
return val as Point;
}
public static byte[] PointAsWkb(Point point)
{
var writer = new NetTopologySuite.IO.WKBWriter();
return writer.Write(point);
}
}
}
正如你所看到的,这里没有什么特别的。 这段代码就位你应该有完整的 CRUDS。 如果您还需要 db 方面的地理支持(就像我们的团队所做的那样),那么您可以创建一个计算列,使用此数据生成正确的地理类型,如下所示:
ALTER TABLE dbo.tableName
ADD Point AS
CONVERT(
GEOGRAPHY,
CASE WHEN [GeographyWkb] IS NOT NULL THEN
GEOGRAPHY::STGeomFromWKB ( [GeographyWkb], 4326 )
ELSE NULL
END)
EF 将忽略此计算列,您将能够在 db 端使用它。 现在这确实让您使用如何处理空间查询,这留给读者。 有多种方法可以处理它,上面的一些答案显示了其中的一些。 值得注意的是,如果查询很小,您可以使用 NetTopologySuite 在内存中进行,因为该库提供了对联合、交叉等的支持......
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.