是否可以使用Linq to Sql的SqlGeography?

我一直在尝试使用Microsoft.SqlServer.Types.SqlGeography遇到很多问题。 我完全知道在Linq to Sql中对此的支持并不是很好。 我尝试了很多方法,从预期的方式开始( geography数据库类型, SqlGeography CLR类型)。 这会产生NotSupportedException ,这是通过博客广泛讨论的。

然后,我将geography列作为varbinary(max) ,因为geography是存储为二进制的UDT。 这似乎工作正常(使用一些二进制读取和写入扩展方法)。

但是,我现在遇到了一个相当模糊的问题,这似乎并没有发生在很多其他人身上。

System.InvalidCastException:无法将类型为“Microsoft.SqlServer.Types.SqlGeography”的对象强制转换为“System.Byte []”。

迭代查询时, ObjectMaterializer会抛出此错误。 它似乎只发生在包含地理列的表隐式包含在查询中时(即使用EntityRef属性进行连接)。

System.Data.Linq.SqlClient.ObjectReaderCompiler.ObjectReader`2.MoveNext()

我的问题:如果我将geography列检索为varbinary(max) ,我可能会发现反向错误:无法将byte[] SqlGeographySqlGeography 。 我明白了。 我没有。 我对部分LINQ to SQL类有一些隐藏二进制转换的特性……那可能是问题吗?

任何帮助表示赞赏,我知道可能没有足够的信息。

附加function:

  • Visual Studio dbml Designer中具有“服务器数据类型”= geography生成此错误: The specified type 'geography' is not a valid provider type.
  • Visual Studio dbml Designer中没有“服务器数据类型”的geography列会生成此错误: Could not format node 'Value' for execution as SQL.

Linq to SQL不支持空间类型。 支持不是“不太好” – 它不存在。

可以将它们作为BLOB读取,但只需将Linq中的列类型更改为SQL即可完成此操作。 您需要在数据库级别更改查询,以使用CAST语句将列作为varbinary返回。 您可以通过添加计算的varbinary列在表级执行此操作,Linq将很乐意映射到byte[]

换句话说,有些DDL是这样的:

 ALTER TABLE FooTable ADD LocationData AS CAST(Location AS varbinary(max)) 

然后,从Linq to SQL类中删除Location列,并改为使用LocationData

如果您需要访问实际的SqlGeography实例,则需要使用SqlGeography和STAsBinary将其转换为字节数组。

您可以通过将部分Linq扩展为SQL实体类并添加自动转换属性来使此过程更加“自动”:

 public partial class Foo { public SqlGeography Location { get { return SqlGeography.STGeomFromWKB(LocationData, 4326); } set { LocationData = value.STAsBinary(); } } } 

这假设LocationData是计算的varbinary列的名称; 如果您没有在Linq to SQL定义中包含“真实” Location列,则可以在上面的ad-hoc方式中添加它。

另请注意,除了读取和写入之外,您将无法对此列做很多事情; 如果你试图对它进行实际查询(即将它包含在Where谓词中),那么你将得到一个类似的NotSupportedException

如果你想用SqlGeography做的只是跟踪点并利用SQL Server 2008的空间索引,你可以像其他人所说的那样,将你的空间数据列从Linq隐藏到SQL并使用UDF或存储过程。 假设您有一个包含纬度和经度字段的表AddressFields。 将该表添加到DBML文件中,并编写您想要设置纬度和经度字段的任何代码。 然后,下面的SQL代码将向该表添加Geo geogarphy字段,并在数据库中创建一个触发器,该触发器根据纬度和经度字段自动设置Geo字段。 同时,下面的代码还创建了其他有用的UDF和存储过程:DistanceBetween2(我已经有一个DistanceBetween)返回AddressField中表示的地址与指定的纬度/经度对之间的距离; DistanceWithin返回指定英里距离内所有AddressFields的各个字段; UDFDistanceWithin与用户定义的函数相同(如果要将其嵌入到更大的查询中,则非常有用); 和UDFNearestNeighbors从AddressField返回对应于最接近特定点的指定邻居数的字段。 (使用UDFNearestNeighbors的一个原因是,如果您只是通过调用DistanceBetween2来调用order,SQL Server 2008将不会优化其空间索引的使用。)

您需要通过将AddressFields更改为表并自定义要返回的表中的字段来自定义此项(查看对AddressFieldID的引用的代码)。 然后,您可以在数据库上运行它,并将生成的存储过程和UDF复制到DBML上,然后您可以在查询中使用它们。 总的来说,这使您可以非常轻松地利用点的空间索引。

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

– [1]

 --INITIAL AUDIT select * from dbo.AddressFields GO --ADD COLUMN GEO IF EXISTS (SELECT name FROM sysindexes WHERE name = 'SIndx_AddressFields_geo') DROP INDEX SIndx_AddressFields_geo ON AddressFields GO IF EXISTS (SELECT b.name FROM sysobjects a, syscolumns b WHERE a.id = b.id and a.name = 'AddressFields' and b.name ='Geo' and a.type ='U' ) ALTER TABLE AddressFields DROP COLUMN Geo GO alter table AddressFields add Geo geography 

– [2]

 --SET GEO VALUE GO UPDATE AddressFields SET Geo = geography::STPointFromText('POINT(' + CAST([Longitude] AS VARCHAR(20)) + ' ' + CAST([Latitude] AS VARCHAR(20)) + ')', 4326) 

– [3]创建指数

 IF EXISTS (SELECT name FROM sysindexes WHERE name = 'SIndx_AddressFields_geo') DROP INDEX SIndx_AddressFields_geo ON AddressFields GO CREATE SPATIAL INDEX SIndx_AddressFields_geo ON AddressFields(geo) --UPDATE STATS UPDATE STATISTICS AddressFields --AUDIT GO select * from dbo.AddressFields 

– [4]创建程序USP_SET_GEO_VALUE PARA 1 LATITUDE 2 LONGITUDE

 IF EXISTS (SELECT name FROM sysobjects WHERE name = 'USPSetGEOValue' AND type = 'P') DROP PROC USPSetGEOValue GO GO CREATE PROC USPSetGEOValue @latitude decimal(18,8), @longitude decimal(18,8) AS UPDATE AddressFields SET Geo = geography::STPointFromText('POINT(' + CAST(@longitude AS VARCHAR(20)) + ' ' + CAST(@latitude AS VARCHAR(20)) + ')', 4326) WHERE [Longitude] =@longitude and [Latitude] = @latitude GO --TEST EXEC USPSetGEOValue 38.87350500,-76.97627500 GO 

– [5]创建LAT / LONG VALUE CHANGE / INSERT的触发器—> SET GEOCODE

 IF EXISTS (SELECT name FROM sysobjects WHERE name = 'TRGSetGEOCode' AND type = 'TR') DROP TRIGGER TRGSetGEOCode GO CREATE TRIGGER TRGSetGEOCode ON AddressFields AFTER INSERT,UPDATE AS DECLARE @latitude decimal(18,8), @longitude decimal(18,8) IF ( UPDATE (Latitude) OR UPDATE (Longitude) ) BEGIN SELECT @latitude = latitude ,@longitude = longitude from inserted UPDATE AddressFields SET Geo = geography::STPointFromText('POINT(' + CAST(@longitude AS VARCHAR(20)) + ' ' + CAST(@latitude AS VARCHAR(20)) + ')', 4326) WHERE [Longitude] =@longitude and [Latitude] = @latitude END ELSE BEGIN SELECT @latitude = latitude ,@longitude = longitude from inserted UPDATE AddressFields SET Geo = geography::STPointFromText('POINT(' + CAST(@longitude AS VARCHAR(20)) + ' ' + CAST(@latitude AS VARCHAR(20)) + ')', 4326) WHERE [Longitude] =@longitude and [Latitude] = @latitude END GO 

– [6] CREATE PROC USP_SET_GEO_VALUE_INITIAL_LOAD —->仅限一次运行

 IF EXISTS (SELECT name FROM sysobjects WHERE name = 'USPSetAllGeo' AND type = 'P') DROP PROC USPSetAllGeo GO CREATE PROC USPSetAllGeo AS UPDATE AddressFields SET Geo = geography::STPointFromText('POINT(' + CAST([Longitude] AS VARCHAR(20)) + ' ' + CAST([Latitude] AS VARCHAR(20)) + ')', 4326) GO 

– [7] EXISTING PROC DistanceBetween,返回指定的两点之间的距离

–by纬度/经度坐标对。 –ALTER PROC DistanceBetween2

 IF EXISTS (SELECT name FROM sysobjects WHERE name = 'DistanceBetween2' AND type = 'FN') DROP FUNCTION DistanceBetween2 GO CREATE FUNCTION [dbo].[DistanceBetween2] (@AddressFieldID as int, @Lat1 as real,@Long1 as real) RETURNS real AS BEGIN DECLARE @KMperNM float = 1.0/1.852; DECLARE @nwi geography =(select geo from addressfields where AddressFieldID = @AddressFieldID) DECLARE @edi geography = geography::STPointFromText('POINT(' + CAST(@Long1 AS VARCHAR(20)) + ' ' + CAST(@Lat1 AS VARCHAR(20)) + ')', 4326) DECLARE @dDistance as real = (SELECT (@nwi.STDistance(@edi)/1000.0) * @KMperNM) return (@dDistance); END 

GO –TEST

DistanceBetween2 12159,40.75889600,-73.99228900


– [8]创建程序USPDistanceWithin

– 从AddressFields表返回地址列表

IF EXISTS(SELECT name FROM sysobjects WHERE name =’USPDistanceWithin’AND type =’P’)DROP PROCEDURE USPDistanceWithin

 GO CREATE PROCEDURE [dbo].USPDistanceWithin (@lat as real,@long as real, @distance as float) AS BEGIN DECLARE @edi geography = geography::STPointFromText('POINT(' + CAST(@Long AS VARCHAR(20)) + ' ' + CAST(@Lat AS VARCHAR(20)) + ')', 4326) SET @distance = @distance * 1609.344 -- convert distance into meter select AddressFieldID ,FieldID ,AddressString ,Latitude ,Longitude ,LastGeocode ,Status --,Geo from AddressFields a WITH(INDEX(SIndx_AddressFields_geo)) where a.geo.STDistance(@edi) < = @Distance END 

- 测试

- 在3英里USPDistanceWithin 38.90606200,-76.92943500,3 GO - 在5英里USPDistanceWithin 38.90606200,-76.92943500,5 GO - 在10英里内USPDistanceWithin 38.90606200,-76.92943500,10


- [9]创建functionFNDistanceWithin

- 从AddressFields表返回地址列表

IF EXISTS(SELECT name FROM sysobjects WHERE name ='UDFDistanceWithin'AND type ='TF')DROP FUNCTION UDFDistanceWithin

 GO CREATE FUNCTION UDFDistanceWithin (@lat as real,@long as real, @distance as real) RETURNS @AddressIdsToReturn TABLE ( AddressFieldID INT ,FieldID INT ) AS BEGIN DECLARE @edi geography = geography::STPointFromText('POINT(' + CAST(@Long AS VARCHAR(20)) + ' ' + CAST(@Lat AS VARCHAR(20)) + ')', 4326) SET @distance = @distance * 1609.344 -- convert distance into meter INSERT INTO @AddressIdsToReturn select AddressFieldID ,FieldID from AddressFields a WITH(INDEX(SIndx_AddressFields_geo)) where a.geo.STDistance(@edi) < = @Distance RETURN END 

- 测试

- 在3英里内选择*来自UDFDistanceWithin(38.90606200,-76.92943500,3)GO - 在5英里内选择*来自UDFDistanceWithin(38.90606200,-76.92943500,5)GO - 在10英里处选择*来自UDFDistanceWithin(38.90606200,-76.92943500) ,10)


- [9] CREATE FUNCTION UDFNearestNeighbors

- 从AddressFields表返回地址列表

IF EXISTS(SELECT name FROM sysobjects WHERE name ='UDFNearestNeighbors'AND type ='TF')DROP FUNCTION UDFNearestNeighbors

 GO 

IF EXISTS(SELECT name FROM sysobjects WHERE name ='numbers'AND xtype ='u')DROP TABLE number

 GO -- First, create a Numbers table that we will use below. SELECT TOP 100000 IDENTITY(int,1,1) AS n INTO numbers FROM MASTER..spt_values a, MASTER..spt_values b CREATE UNIQUE CLUSTERED INDEX idx_1 ON numbers(n) GO CREATE FUNCTION UDFNearestNeighbors (@lat as real,@long as real, @neighbors as int) RETURNS @AddressIdsToReturn TABLE ( AddressFieldID INT ,FieldID INT ) AS BEGIN DECLARE @edi geography = geography::STPointFromText('POINT(' + CAST(@Long AS VARCHAR(20)) + ' ' + CAST(@Lat AS VARCHAR(20)) + ')', 4326) DECLARE @start FLOAT = 1000; WITH NearestPoints AS ( SELECT TOP(@neighbors) WITH TIES *, AddressFields.geo.STDistance(@edi) AS dist FROM Numbers JOIN AddressFields WITH(INDEX(SIndx_AddressFields_geo)) ON AddressFields.geo.STDistance(@edi) < @start*POWER(2,Numbers.n) ORDER BY n ) INSERT INTO @AddressIdsToReturn SELECT TOP(@neighbors) AddressFieldID ,FieldID FROM NearestPoints ORDER BY n DESC, dist RETURN END 

- 测试

--50邻居选择*来自UDFNearestNeighbors(38.90606200,-76.92943500,50)GO - 200邻居选择*来自UDFNearestNeighbors(38.90606200,-76.92943500,200)GO