У меня было довольно много проблем при попытке использовать Microsoft.SqlServer.Types.SqlGeography
. Я знаю очень хорошо, что поддержка этого в Linq к Sql не является большой. Я попробовал многочисленные пути, начав с того, что было бы ожидаемый путь (Тип БД geography
, Тип CLR SqlGeography
). Это производит NotSupportedException
, который широко обсужден с помощью блогов.
Я затем спустился по пути обработки geography
столбец как a varbinary(max)
, как geography
UDT, сохраненный как двоичный файл. Это, кажется, хорошо работает (с некоторым двоичным чтением и записью дополнительных методов).
Однако я теперь сталкиваюсь с довольно неясной проблемой, которой, кажется, не произошло со многими другими людьми.
Система. InvalidCastException: Не мог бросить объект типа 'Microsoft. SqlServer. Типы. SqlGeography' для ввода 'Системы. Байт []'.
Эта ошибка брошена от ObjectMaterializer
при итерации через запрос. Это, кажется, происходит только, когда таблицы, содержащие столбцы географии, включены в запрос неявно (т.е. использование EntityRef<>
свойства, чтобы сделать соединения).
Система. Данные. Linq. SqlClient. ObjectReaderCompiler. ObjectReader '2. MoveNext ()
Мой вопрос: Если я получаю geography
столбец как varbinary(max)
, Я мог бы ожидать обратную ошибку: не может бросить byte[]
кому: SqlGeography
. То, что я понял бы. Это я не делаю. У меня действительно есть некоторый properies на частичном LINQ к классам SQL, которые скрывают двоичное преобразование... это могла быть проблема?
Любая справка ценила, и я знаю, что существует, достаточно вероятно, информация.
Отдельно оплачиваемые предметы:
geography
столбец в Visual Studio dbml Разработчик с 'Типом Серверных данных' = geography
генерирует эту ошибку: The specified type 'geography' is not a valid provider type.
geography
столбец в Visual Studio dbml Разработчик без 'Типа Серверных данных' генерирует эту ошибку: Could not format node 'Value' for execution as SQL.
Пространственные типы не поддерживаются Linq to SQL. Поддержка не "не очень" - она отсутствует.
Вы можете читать их как BLOBs, но вы не можете сделать это, просто изменив тип столбца в Linq to SQL. Вам нужно изменить свои запросы на уровне базы данных, чтобы вернуть столбец как varbinary
, используя оператор CAST
. Вы можете сделать это на уровне таблицы, добавив вычисляемый varbinary
столбец, который Linq с удовольствием отобразит в byte[]
.
Другими словами, некоторые DDL вроде этого:
ALTER TABLE FooTable
ADD LocationData AS CAST(Location AS varbinary(max))
Затем удалите колонку Location
из класса Linq to 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
; вы не включаете "настоящий" столбец Location
в определение Linq to SQL, вы добавляете его специальным способом, описанным выше.
Заметьте также, что вы не сможете сделать с этим столбцом ничего, кроме чтения и записи в него; если вы попытаетесь выполнить запрос по нему (т. е. включить его в предикат 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 заключается в том, что SQL Server 2008 не будет оптимизировать использование пространственного индекса, если вы просто вызовете порядок, вызвав DistanceBetween2.)
Вам нужно будет настроить это, изменив AddressFields в вашей таблице и настроив поля из этой таблицы, которые вы хотите вернуть (посмотрите в коде ссылки на AddressFieldID).Затем вы можете запустить это в своей базе данных и скопировать полученные хранимые процедуры и UDF в свой DBML, а затем использовать их в запросах. В целом, это позволяет довольно легко использовать пространственный индекс точек.
-----------------------------------------------------------------------------------------
--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
--SET GEO VALUE
GO
UPDATE AddressFields
SET Geo = geography::STPointFromText('POINT(' + CAST([Longitude] AS VARCHAR(20)) + ' ' +
CAST([Latitude] AS VARCHAR(20)) + ')', 4326)
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
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
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
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] СУЩЕСТВУЮЩИЙ ПРОЦЕСС DistanceBetween, который возвращает расстояние между двумя точками, указанное
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
ЕСЛИ СУЩЕСТВУЕТ (ВЫБЕРИТЕ имя ИЗ sysobjects WHERE name = USPDistanceWithin 'И тип =' 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
GO
- TEST
- в пределах 3 миль Расстояние по USP: в пределах 38.90606200, -76.92943500,3 ИДТИ - в пределах 5 миль Расстояние по USP: в пределах 38.90606200, -76.92943500,5 ИДТИ --в пределах 10 миль USPDistanceWithin 38.90606200, -76.92943500,10
- [9] СОЗДАТЬ ФУНКЦИЮ FNDistanceWithin
ЕСЛИ СУЩЕСТВУЕТ (ВЫБЕРИТЕ имя ИЗ sysobjects WHERE name = 'UDFDistanceWithin' AND type = 'UDFDistanceWithin' 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 миль выберите * из UDFDistanceWithin (38.90606200, -76.92943500,3) ИДТИ - в пределах 5 миль выберите * из UDFDistanceWithin (38.90606200, -76.92943500,5) ИДТИ --в пределах 10 миль выберите * из UDFDistanceWithin (38.90606200, -76.92943500,10)
- [9] СОЗДАТЬ ФУНКЦИЮ UDFNearestNeighbors
ЕСЛИ СУЩЕСТВУЕТ (ВЫБЕРИТЕ имя FROM sysobjects 'UDERFORSN 'И тип =' TF ') ФУНКЦИЯ УДАЛЕНИЯ UDFNearestNeighbors
GO
ЕСЛИ СУЩЕСТВУЕТ (ВЫБЕРИТЕ имя ИЗ sysobjects, ГДЕ name = 'numbers' И 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
GO
- TEST
- 50 соседей выберите * из UDFNearestNeighbors (38.90606200, -76.92943500,50) ИДТИ --200 соседей выберите * из UDFNearestNeighbors (38.90606200, -76.92943500,200) GO