Можно ли использовать SqlGeography с Linq to Sql?
У меня было немало проблем с использованием 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, которые скрывают двоичное преобразование... могут ли это быть проблемой?
Любая помощь оценивается, и я знаю, что, вероятно, информации недостаточно.
Дополнительно:
- A
geography
в конструкторе dbml Visual Studio с "Тип данных сервера" = geography
генерирует эту ошибку: The specified type 'geography' is not a valid provider type.
- Столбец
geography
в конструкторе dbml Visual Studio без "Тип данных сервера" генерирует эту ошибку: Could not format node 'Value' for execution as SQL.
Ответы
Ответ 1
Пространственные типы не поддерживаются Linq to SQL. Поддержка не "не велика" - она отсутствует.
Вы можете читать их как BLOB, но вы не можете этого сделать, просто изменив тип столбца в Linq на SQL. Вам нужно изменить свои запросы на уровне базы данных, чтобы вернуть столбец как varbinary
, используя оператор CAST
. Вы можете сделать это на уровне таблицы, добавив вычисленный столбец varbinary
, который Linq с радостью сопоставляет с byte[]
.
Другими словами, некоторые DDL такие:
ALTER TABLE FooTable
ADD LocationData AS CAST(Location AS varbinary(max))
Затем удалите столбец Location
из класса Linq в SQL и вместо этого используйте LocationData
.
Если вам нужен доступ к фактическому экземпляру SqlGeography
, вам нужно будет преобразовать его в массив байтов и из него, используя STGeomFromWKB и STAsBinary.
Вы можете сделать этот процесс немного более "автоматическим", расширив частичный класс объектов Linq to SQL и добавив свойство автоматической конвертации:
public partial class Foo
{
public SqlGeography Location
{
get { return SqlGeography.STGeomFromWKB(LocationData, 4326); }
set { LocationData = value.STAsBinary(); }
}
}
Предполагается, что LocationData
- это имя вычисленного столбца varbinary
; вы не включаете столбец "real" Location
в определении Linq to SQL, вы добавляете его в ad-hoc выше.
Обратите внимание также, что вы не сможете многое сделать с этим столбцом, кроме чтения и записи; если вы попытаетесь на самом деле запросить его (т.е. включить его в предикат Where
), вы просто получите аналогичный NotSupportedException
.
Ответ 2
Если все, что вы хотите сделать с SqlGeography, это точки отслеживания и использовать пространственные индексы SQL Server 2008, вы можете, как заметили другие, скрыть столбец пространственных данных от Linq to SQL и использовать UDF или хранимые процедуры. Предположим, что у вас есть таблица AddressFields, которая включает в себя широту и долготу. Добавьте эту таблицу в ваш файл DBML и напишите любой код, который вы хотите установить для полей "Широта и долгота". Затем приведенный ниже код SQL добавит в эту таблицу поле Geo Geogarphy и создаст триггер в базе данных, который автоматически устанавливает поле Geo на основе полей широты и долготы. Между тем, приведенный ниже код также создает другие полезные UDF и хранимые процедуры: DistanceBetween2 (у меня уже было DistanceBetween) возвращает расстояние между адресом, представленным в AddressField, и указанной паркой широты/долготы; DistanceWithin возвращает различные поля из всех адресных полей в пределах заданной мили; UDFDistanceWithin делает то же самое, что и пользовательская функция (полезно, если вы хотите встроить ее в более крупный запрос); а UDFNearestNeighbors возвращает поля из AddressField, соответствующие указанному числу соседей, ближайших к определенной точке. (Одной из причин использования UDFNearestNeighbors является то, что SQL Server 2008 не будет оптимизировать использование пространственного индекса, если вы просто вызываете порядок, вызывая DistanceBetween2.)
Вам нужно будет настроить это, изменив 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] 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] [email protected] and [Latitude] = @latitude
GO
--TEST
EXEC USPSetGEOValue 38.87350500,-76.97627500
GO
- [5] СОЗДАТЬ ТРИГГЕР НА LAT/LONG VANUE 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] [email protected] 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] [email protected] 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] СУЩЕСТВУЮЩИЙ PROC DistanceBetween, который возвращает расстояние между двумя указанными точками
- по координатам пары широты/долготы. --ALTER PROC РасстояниеBetween2
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
РасстояниеBetween2 12159,40.75889600, -73.99228900
- [8] ПРОЦЕДУРА СОЗДАНИЯ USPDistanceWithin
- ВОЗВРАЩАЕТ СПИСОК АДРЕСА из таблицы AddressFields
IF EXISTS (SELECT name FROM sysobjects WHERE name = 'USPDistanceWithin' AND type = 'P') ПРОЦЕДУРА УБЫТКА 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
ИДТИ
- в 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
ГО
- ТЕСТ
- в пределах 3 миль
выберите * из UDFDistanceWithin (38.90606200, -76.92943500,3)
ИДТИ
- в 5 милях
выберите * из 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
ГО
- ТЕСТ
- 50 соседей
выберите * из UDFNearestNeighbors (38.90606200, -76.92943500,50)
ИДТИ
- 200 соседей
выберите * из UDFNearestNeighbors (38.90606200, -76.92943500,200)
GO