实体框架核心:此平台不支持类型Udt。 (空间数据 - 地理)

时间:2017-07-02 18:04:08

标签: c# entity-framework asp.net-web-api entity-framework-core

我正在试验实体框架核心,偶然发现了我以前从未见过的错误,无法弄清楚如何修复它。 我使用.net Core Web API 2.0与EntityFramework Core 2.00-preview2-final

这是一个触发错误的简单示例。

(概念:从数据库中获取用户的简单端点)

错误: System.PlatformNotSupportedException:此平台不支持类型Udt。

有什么建议吗?

问题是我在我的数据库中使用地理位置,但我在模型中将其用作字符串,因为实体框架核心还不支持空间数据......

任何方法可以保持这块蛋糕的美味而不会摆脱地理位置,这是一个重要特征吗?

编辑:请参阅我当前解决方案的答案

3 个答案:

答案 0 :(得分:8)

好的,我是如何解决的:

目的是将地理位置保留在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)生成迁移(添加迁移迁移名称)

5)转到您的迁移文件1231randomnumbers1231_migrationname.cs 并添加以下内容(这样我们创建另一个名为Location的地理类型列),然后更新您的数据库(update-database):

migrationBuilder.Sql(@"ALTER TABLE [dbo].[User] ADD [Location] geography NULL");

6)(可选)我创建了一个静态类来更新数据库,如果在mulple表中有一个Location列,则会很方便。

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,你必须找到另一种方法来替换&#39;&#39;用&#39;。&#39;。一个很好的旧时尚.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;
}

答案 1 :(得分:4)

更新:由于EF Core 2.2支持空间数据!:

http://portainer/#/stacks/kolibry-acc_8ssg5qantkr2dgrbxsamsikf6

-------------------------

Eli,tnx因为你是解决方案。对我来说,这几乎是完美的解决方案。我有两个问题:

<强>问题

  1. 另一个应用程序也直接插入数据库(临时解决方案,将来会更改)。
  2. 获取前50个实体时,数据必须按距离排序,因此将返回最近的50个实体。
  3. <强>解决方案

    1. 不是从代码更新位置表,而是在访问表上使用触发器。此触发器将填充或插入,删除或更新Location表。因此,创建,更新,删除功能除了保存实体外无需执行任何操作。
    2. 
          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
      
      1. 获取前50个查询必须是原始SQL查询,如下所示:
      2. 
        
            _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; }
            }
        

答案 2 :(得分:0)

这些解决方案有效但如果您正在寻找其他方法,这是另一种解决方案。由于此时EF核心2不支持地理类型,因此您可以将NetTopologySuite用于所有服务器端地理支持。

如果您的表需要地理列添加属性,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。如果您还需要数据库端的地理支持(就像我们的团队那样),那么您可以创建一个计算列,使用此数据生成正确的地理类型,如下所示:

ALTER TABLE dbo.tableName
    ADD Point AS 
        CONVERT(
            GEOGRAPHY,
            CASE WHEN [GeographyWkb] IS NOT NULL THEN 
                GEOGRAPHY::STGeomFromWKB ( [GeographyWkb], 4326 )
            ELSE NULL
            END)

EF将忽略此计算列,您将能够使用它的数据库侧。现在,这确实可以用于处理空间查询,并留给读者。有很多方法可以处理它,上面的一些答案显示了其中一些。值得注意的是,如果查询很小,你可以使用NetTopologySuite在内存中进行,因为库提供了对工会,交叉点等的支持......