10

我在尝试使用Microsoft.SqlServer.Types.SqlGeography. 我很清楚 Linq to Sql 对此的支持不是很好。我尝试了很多方法,从预期的方法开始(数据库类型geography,CLR 类型SqlGeography)。这会产生NotSupportedException,通过博客广泛讨论。

然后,我沿着将geography列视为的路径varbinary(max),将geographyUDT 存储为二进制文件。这似乎工作正常(使用一些二进制读写扩展方法)。

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

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 类上确实有一些属性......这些可能是问题吗?

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

附加功能:

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

2 回答 2

17

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

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 值更改/插入时创建触发器 ---> 设置地理代码

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

--按纬度/经度坐标对。--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

去——测试

距离Between2 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] 创建函数 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

- 测试

-- 在 3 英里内选择 * 从 UDFDistanceWithin(38.90606200,-76.92943500,3) 开始 -- 在 5 英里内选择 * 从 UDFDistanceWithin(38.90606200,-76.92943500,5) 开始 -- 在 10 英里内选择 * 从 UDFDistanceWithin( 38.920606200,-7900. ,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 numbers

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

于 2010-06-03T18:14:56.293 回答
13

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

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

换句话说,一些像这样的 DDL:

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

然后,Location从 Linq to SQL 类中删除该列,然后LocationData改用。

如果您随后需要访问实际实例,则需要使用STGeomFromWKBSTAsBinarySqlGeography将其与字节数组相互转换。

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

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

这假定这LocationData是计算varbinary列的名称;您没有Location在 Linq to SQL 定义中包含“真实”列,而是以上面的临时方式添加它。

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

于 2010-05-17T00:16:59.920 回答