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

时间:2010-05-16 22:41:42

标签: c# sql-server linq-to-sql geography sqlgeography

我在尝试使用Microsoft.SqlServer.Types.SqlGeography时遇到了一些问题。我完全知道在Linq to Sql中对此的支持并不是很好。我尝试了很多方法,从预期的方式开始(数据库类型为geography,CLR类型为SqlGeography)。这会产生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[]转换为SqlGeography。我明白了。我没有。我对部分LINQ to SQL类有一些隐藏二进制转换的特性......那可能是问题吗?

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

附加功能:

  • Visual Studio dbml Designer中的geography列,其中“服务器数据类型”= 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.

2 个答案:

答案 0 :(得分:16)

如果你想用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将不会优化其空间索引的使用。)

您需要通过将AddressField更改为表并自定义要返回的表中的字段来自定义此项(查看对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] CREATE INDEX

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的触发器---&gt; 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 ----&gt;一次只跑一次

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,返回指定的两点之间的距离

- 来自纬度/经度坐标对。 --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 - 测试

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

GO

- TEST

- 3英里内 USPDistanceWithin 38.90606200,-76.92943500,3 走 - 距离5英里 USPDistanceWithin 38.90606200,-76.92943500,5 走 - 距离10英里 USPDistanceWithin 38.90606200,-76.92943500,10


- [9]创建功能FNDistanceWithin

- 从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

GO

- TEST

- 3英里内 select * from UDFDistanceWithin(38.90606200,-76.92943500,3) 走 - 距离5英里 select * from UDFDistanceWithin(38.90606200,-76.92943500,5) 走 - 距离10英里 从UDFDistanceWithin中选择*(38.90606200,-76.92943500,10)


- [9]创建功能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号码

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

GO

- TEST

- 50个邻居 select * from UDFNearestNeighbors(38.90606200,-76.92943500,50) 走 --200个邻居 select * from UDFNearestNeighbors(38.90606200,-76.92943500,200) GO

答案 1 :(得分:13)

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

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

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

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

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

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

通过将部分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列,而是以上面的临时方式添加它。

另请注意,除了读取和写入此列之外,您将无法对此列执行任何操作;如果您尝试对其进行实际查询(即将其包含在Where谓词中),那么您将获得类似的NotSupportedException