Начиная с версии 2008 (и пока что заканчивая ей) MS SQL Server имеет встроенную поддержку пространственных данных. Прекрасно!
На данный момент времени уже существует несколько СУБД, предлагающих индексированное хранение пространственных данных. Наверное, самые популярные из них, это: «народная» MySql и PostGIS.
Программируя на c#, естественно, в очень многих случаях, отдаёшь предпочтение продуктам и решениям Microsoft. Причины просты: полнее поддержка одних технологий другими, хорошая документация, более полная реализация, например провайдеров данных, и гораздо меньшая глючность. Я выбрал SQL Server. Заодно захотелось освоить LINQ в общем и LINQ to SQL в. частности.
Поначалу всё было хорошо. Для меня хороший старт сделала, обнаруженная на msdn, статья «LINQ to SQL: .NET
Но я не сильно удивился, когда «всё хорошо» закончилось.
Для хранения геометрических данных в SQL Server были введены два дополнительных типа: geometry и geography. Первый используется для хранения геометрических объектов, описанных в декартовой системе координат, а второй — для геометрических объектов заданных географическими координатами (широта/долгота).
Такое разделение, по всей видимости, пришлось сделать
Оказалось, что LINQ to SQL не понимает этих типов данных и работать с ними, а также со встроенными геометрическими функциями, отказывается. Хотя, наверное, правильнее сказать, что их не понимает провайдер. В любом случае, уверен, что поддерживаться эти данные будут, но сейчас такой поддержки нет.
Я не смог найти в интернет решения, обходящего эту проблему, поэтому пришлось изобрести его самому. Здесь нет никаких удивительных ходов, но есть детали, которые, думаю, будут интересны. Также в этой большой заметке, для вашего интереса, я
База данных
Для примера будем использовать следующую таблицу.Для её создания использовался следующий скрипт.
- USE ExampleDatabase;
- GO
- --Create table
- CREATE TABLE Boundaries_Country(
- FeatureID UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
- CountryName VARCHAR(100) NOT NULL UNIQUE,
- CountryBoundary GEOGRAPHY NOT NULL
- )
- CREATE SPATIAL INDEX SpatialIndex
- ON Boundaries_Country (CountryBoundary);
- GO
Чтобы было с чем работать, я заполнил таблицу мультиполигонами стран мира, shape-файл для которых был найден на просторах интернет. Несколько стран не захотели конвертироваться — я не стал разбираться почему, было не важно, хотя за Россию, безусловно, обидно.
SQL Server имеет симпатичный встроенный просмотрщик (ну вот, теперь все видят, что я пишу с ошибками).

Начало работы с LINQ to SQL
Для работы с LINQ to SQL в проект нужно добавить ссылки на две сборки: System.Data.Linq и Microsoft.SqlServer.Types. Если с первой библиотекой проблем нет (её можно найти на вкладке «.NET» формы «Add Reference» — добавления ссылки на используемую в проекте библиотеку), то вторую нужно будет поискать в директории «C:\Program Files\Microsoft SQL Server\100\SDK\Assemblies\». Для того, чтобы последняя сборка впредь отображалась во вкладке «.NET» формы добавления сборок, нужно её зарегистрировать один раз с помощью утилиты gacutil.Первый шаг в использовании LINQ to SQL — это создание классов-отображений для таблиц базы данных.
На одну таблицу — один класс.
- using System;
- using System.Data.Linq.Mapping;
- using Microsoft.SqlServer.Types;
- namespace MyNamespace
- {
- [Table()]
- public sealed class Boundaries_Country
- {
- [Column(AutoSync = AutoSync.OnInsert, DbType = "uniqueidentifier", IsPrimaryKey = true, IsDbGenerated = true, UpdateCheck = UpdateCheck.Never)]
- public Guid FeatureID;
- [Column(DbType = "varchar(100)", CanBeNull = false)]
- public string CountryName;
- [Column(/*DbType = "geography", */CanBeNull = false)]
- public SqlGeography CountryBoundary;
- }
- }
В строке 16, при описании атрибута для поля, содержащего пространственные данные, по идее, я должен указать тип данных geography, но поскольку поддержки этого типа данных ещё нет, то я его и не указываю.
Ещё один класс нужен для создания контекста базы данных. Мы можем использовать уже имеющийся DataContext, но лучше сделать своего наследника для строгой типизации.
- using System.Data.Linq;
- namespace MyNamespace
- {
- public class ExampleDatabase: DataContext
- {
- public Table<Boundaries_Country> BoundariesCountry;
- public ExampleDatabase(string connectionString)
- : base(connectionString)
- {
- }
- }
- }
- static void Main(string[] args)
- {
- ExampleDatabase db = new ExampleDatabase(@"...");
- var q = from item in db.BoundariesCountry
- where item.CountryName.StartsWith("C")
- select item;
-
- foreach (var item in q)
- Console.WriteLine(item.CountryName);
- }
Один интересный момент. Если в режиме отладки остановить выполнение программы на строке 9 и просмотреть содержание переменной q, то мы увидим сформированный LINQ to SQL запрос.
LINQ to SQL: работа с пространственными данными
Рассмотрим запрос, выбирающий из базы данных страны, попавшие в заданный прямоугольник, и название которых начинается на букву «С».Прямоугольник задан полигоном (WKT-представление): POLYGON ((40 -28, 40 30, 5 30, 5 -28, 40 -28)).
- var q = from item in db.BoundariesCountry
- where item.CountryName.StartsWith("C") &&
item.CountryBoundary.STIntersects(sqlEnvelope).Value - select item;
- foreach (var item in q)
- Console.WriteLine(item.CountryName);
Во время выполнения, на строке 5, когда от LINQ to SQL потребуется отправить запрос на сервер, будет выброшено исключение: "Method 'System.Data.SqlTypes.SqlBoolean STIntersects(Microsoft.SqlServer.Types.SqlGeography)' has no supported translation to SQL.".
Чтобы решить эту проблему мы будем использовать хранимые процедуры и table-valued функции, а пересылать геометрические объекты на сервер будем в хорошо понятном SQL Server'у бинарном формате WKB.
Хранимые процедуры
Для выборки геометрических фигур по критерию попадания в заданный прямоугольник для одной конкретной таблицы, достаточно создать следующую простую хранимую процедуру.- CREATE PROCEDURE [dbo].[sp_bbx_Boundaries_Country]
- @boundingBox varbinary(max)
- AS
- BEGIN
- SET NOCOUNT ON;
- SELECT *
- FROM dbo.Boundaries_Country
- WHERE GEOGRAPHY::STGeomFromWKB(@boundingBox,
4326).STIntersects(CountryBoundary) = 1; - RETURN;
- END
В программе, в классе, реализующем DataContext (у нас этот класс называется ExampleDatabase), опишем обёртку для вызова этой процедуры.
- [Function()]
- public ISingleResult<Boundaries_Country> sp_bbx_Boundaries_Country(
[Parameter(DbType = "varbinary(max)")] byte[] boundingBox) - {
- IExecuteResult execResult = this.ExecuteMethodCall(this, ((MethodInfo)
(MethodInfo.GetCurrentMethod())), boundingBox); - ISingleResult<Boundaries_Country> result =
((ISingleResult<Boundaries_Country>)execResult.ReturnValue); - return result;
- }
В строке 4 вызывается хранимая процедура и результат сохраняется в execResult, затем, в строке 5, он преобразуется к требуемому типу данных и возвращается в основную программу.
Пользуемся этой «радостью» следующим образом:
- var q = from item in db.sp_bbx_Boundaries_Country(
sqlEnvelope.STAsBinary().Buffer) - where item.CountryName.StartsWith("C")
- select item;
- foreach (var item in q)
- Console.WriteLine(item.CountryName);
Замечание, если у вас, как у меня в рабочем проекте, несколько таблиц с географическими данными, то имеются следующие варианты.
- Для каждой таблицы создать свою хранимую процедуру, а в программе для каждой хранимой процедуры свою функцию-обёртку. Можно упростить жизнь пользователю api, если написать "центральный" generic-метод в котором, по актуальному типу, используемому при вызове generic-метода, будут вызываться приватные метод-обёртки хранимых процедур и выполняться необходимые приведения типов.
- Написать одну хранимую процедуру с использованием динамического sql. В программе нужно будет сделать один generic-метод, из которого также, как и в предыдущем варианте будут вызывать специализированные (по типу данных) методы-обёртки вокруг одной и той же процедуры (с наскока уйти от этого не удалось, воевать не стал).
Хранимые процедуры — это хорошо, но при использовании в LINQ to SQL, в описанной манере, у них есть один существенный недостаток: хранимые процедуры исполняются сразу и с сервера на клиент пересылаются все, попавшие в заданный регион, страны и уже потом над этим массивом выполняется дополнительная фильтрация. Т.е. трансляции в SQL всего LINQ-выражения не происходит. Для ухода от этой проблемы мы можем использовать inline-функции SQL Server'а.
Table-valued функции
Table-valued функция, извлекающая из таблицы базы данных записи по критерию попадания в заданный регион, может быть создана следующим образом.
- CREATE FUNCTION [dbo].[f_bbx_Boundaries_Country]
- (
- @boundingBox varbinary(max)
- )
- RETURNS TABLE
- AS
- RETURN
- (
- SELECT *
- FROM dbo.Boundaries_Country
- WHERE GEOGRAPHY::STGeomFromWKB(
@boundingBox, 4326).STIntersects(CountryBoundary) = 1 - )
Для функции также нужно будет создать свою обёртку.
- [Function(IsComposable = true)]
- public IQueryable<Boundaries_Country> f_bbx_Boundaries_Country(
[Parameter(DbType = "varbinary(max)")] byte[] boundingBox) - {
- return this.CreateMethodCallQuery<Boundaries_Country>(this,
((MethodInfo)(MethodInfo.GetCurrentMethod())), boundingBox); - }
Смотрим пример.
- var q = from item in db.f_bbx_Boundaries_Country(
sqlEnvelope.STAsBinary().Buffer) - where item.CountryName.StartsWith("C")
- select item;
- foreach (var item in q)
- Console.WriteLine(item.CountryName);
И в отладке видим прекрасную картину (всё linq-выражение было транслировано в sql-запрос):

Всё, мне больше нечего сказать народу.


