10

を使用しようとしてかなりの問題が発生しましたMicrosoft.SqlServer.Types.SqlGeography。私は、Linq to Sql でのこれに対するサポートが優れていないことをよく知っています。私は、期待される方法(データベースの種類geography、CLRの種類SqlGeography)から始めて、さまざまな方法を試しました。これにより、NotSupportedExceptionブログで広く議論されている が生成されます。

次に、バイナリとして格納されたUDTと同様に、geography列を として扱う道をたどりました。これは正常に動作するようです (いくつかのバイナリ読み取りおよび書き込み拡張メソッドを使用)。varbinary(max)geography

しかし、私は今、かなりあいまいな問題に直面しています。これは、他の多くの人には起こらなかったようです。

System.InvalidCastException: タイプ 'Microsoft.SqlServer.Types.SqlGeography' のオブジェクトをタイプ 'System.Byte[]' にキャストできません。

ObjectMaterializerこのエラーは、クエリを反復処理するときにスローされます。地理列を含むテーブルが暗黙的にクエリに含まれている場合にのみ発生するようです (つまり、EntityRef<>プロパティを使用して結合を行います)。

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

私の質問:geography列を として取得している場合、逆のエラーが発生する可能性があります: にキャストできvarbinary(max)ません。それは私が理解するでしょう。これは私はしません。バイナリ変換を隠す部分的な LINQ to SQL クラスにいくつかのプロパティがあります...それらが問題になる可能性がありますか?byte[]SqlGeography

助けていただければ幸いです。おそらく十分な情報がないことは承知しています。

エクストラ:

  • geography'Server Data Type' = の Visual Studio dbml Designerの列で、次のgeographyエラーが生成されます。The specified type 'geography' is not a valid provider type.
  • 'Server Data Type' のない Visual Studio dbml Designerのgeography列は、次のエラーを生成します。Could not format node 'Value' for execution as SQL.
4

2 に答える 2

17

SqlGeography でやりたいことがトラック ポイントだけで、SQL Server 2008 の空間インデックスを利用する場合は、他の人が指摘しているように、空間データ列を Linq から SQL に隠し、UDF またはストアド プロシージャを使用できます。緯度と経度のフィールドを含むテーブル AddressFields があるとします。そのテーブルを DBML ファイルに追加し、緯度と経度のフィールドを設定する任意のコードを記述します。次に、以下の SQL コードは、Geo geogarphy フィールドをそのテーブルに追加し、Latitude フィールドと Longitude フィールドに基づいて Geo フィールドを自動的に設定するトリガーをデータベースに作成します。一方、以下のコードは、他の便利な UDF とストアド プロシージャも作成します。DistanceBetween2 (既に DistanceBetween を持っていました) は、AddressField で表される住所と指定された緯度/経度のペアとの間の距離を返します。DistanceWithin は、指定されたマイル距離内のすべての AddressFields からさまざまなフィールドを返します。UDFDistanceWithin は、ユーザー定義関数と同じことを行います (これをより大きなクエリに埋め込みたい場合に便利です)。また、UDFNearestNeighbors は、特定のポイントに最も近い、指定された数の近傍に対応する AddressField からフィールドを返します。(UDFNearestNeighbors を使用する理由の 1 つは、DistanceBetween2 を呼び出して順序を呼び出すだけでは、SQL Server 2008 が空間インデックスの使用を最適化しないことです)。UDFDistanceWithin は、ユーザー定義関数と同じことを行います (これをより大きなクエリに埋め込みたい場合に便利です)。また、UDFNearestNeighbors は、特定のポイントに最も近い、指定された数の近傍に対応する AddressField からフィールドを返します。(UDFNearestNeighbors を使用する理由の 1 つは、DistanceBetween2 を呼び出して順序を呼び出すだけでは、SQL Server 2008 が空間インデックスの使用を最適化しないことです)。UDFDistanceWithin は、ユーザー定義関数と同じことを行います (これをより大きなクエリに埋め込みたい場合に便利です)。また、UDFNearestNeighbors は、特定のポイントに最も近い、指定された数の近傍に対応する AddressField からフィールドを返します。(UDFNearestNeighbors を使用する理由の 1 つは、DistanceBetween2 を呼び出して順序を呼び出すだけでは、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] CREATE PROCEDURE 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] 緯度/経度値の変更/挿入でトリガーを作成 ---> ジオコードを設定

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 ----> 1 回のみ実行

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] 既存の PROC DistanceBetween。指定された 2 点間の距離を返します。

-- 緯度/経度の座標ペアによる。--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

行く - テスト

DistanceBetween2 12159,40.75889600,-73.99228900


--[8] CREATE PROCEDURE USPDistanceWithin

-- AddressFields テーブルからアドレスのリストを返します

存在する場合 (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] CREATE FUNCTION FNDistanceWithin

-- AddressFields テーブルからアドレスのリストを返します

存在する場合 (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 マイル以内 select * from UDFDistanceWithin(38.90606200,-76.92943500,3) GO -- 5 マイル以内 select * from UDFDistanceWithin( 38.90606200,-76.92943500,5) GO -- 10 マイル以内 select * from UDFDistanceWithin( 38.90606200,-4356.9) ,10)


--[9] CREATE FUNCTION UDFNearestNeighbors

-- AddressFields テーブルからアドレスのリストを返します

存在する場合 (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を追加することで、テーブル レベルでこれを行うことができます。varbinarybyte[]

つまり、次のような DDL があります。

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

次に、LocationLinq to SQL クラスから列を削除し、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

于 2010-05-17T00:16:59.920 に答える