LINQ to SQL и пространственные данные SQL ServerИсточник: realcoding RollingStone
Начиная с версии 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 не понимает этих типов данных и работать с ними, а также со встроенными геометрическими функциями, отказывается. Хотя, наверное, правильнее сказать, что их не понимает провайдер. В любом случае, уверен, что поддерживаться эти данные будут, но сейчас такой поддержки нет. Я не смог найти в интернет решения, обходящего эту проблему, поэтому пришлось изобрести его самому. Здесь нет никаких удивительных ходов, но есть детали, которые, думаю, будут интересны. Также в этой большой заметке, для вашего интереса, я База данныхДля примера будем использовать следующую таблицу. Для её создания использовался следующий скрипт.
В строке 11 для поля CountryBoundary c типом данных geography, создаётся пространственный индекс с настройками по умолчанию. Чтобы было с чем работать, я заполнил таблицу мультиполигонами стран мира, shape-файл для которых был найден на просторах интернет. Несколько стран не захотели конвертироваться - я не стал разбираться почему, было не важно, хотя за Россию, безусловно, обидно. SQL Server имеет симпатичный встроенный просмотрщик (ну вот, теперь все видят, что я пишу с ошибками). Начало работы с LINQ to SQLДля работы с LINQ to SQL в проект нужно добавить ссылки на две сборки: System.Data.Linq и Microsoft.SqlServer.Types. Если с первой библиотекой проблем нет (её можно найти на вкладке ".NET" формы "Add Reference" - добавления ссылки на используемую в проекте библиотеку), то вторую нужно будет поискать в директории "C:Program FilesMicrosoft SQL Server100SDKAssemblies". Для того, чтобы последняя сборка впредь отображалась во вкладке ".NET" формы добавления сборок, нужно её зарегистрировать один раз с помощью утилиты gacutil. Первый шаг в использовании LINQ to SQL - это создание классов-отображений для таблиц базы данных. На одну таблицу - один класс.
Над объявлением класса и над полями расставлены атрибуты. Например, в строке 7, атрибут Table указывает, что этот класс ассоциирован с таблицей в базе данных. Если имя класса совпадает с именем таблицы, то атрибут можно записывать так, как у меня, а если нет, то нужно будет указать дополнительное свойство Name: [Table(Name = "Boundaries_Country")]. В строке 16, при описании атрибута для поля, содержащего пространственные данные, по идее, я должен указать тип данных geography, но поскольку поддержки этого типа данных ещё нет, то я его и не указываю. Ещё один класс нужен для создания контекста базы данных. Мы можем использовать уже имеющийся DataContext, но лучше сделать своего наследника для строгой типизации.
Пример, выгребем из базы данных всё, что есть, но так, чтобы название страны начиналось с буквы "С".
Один интересный момент. Если в режиме отладки остановить выполнение программы на строке 9 и просмотреть содержание переменной q, то мы увидим сформированный LINQ to SQL запрос. LINQ to SQL: работа с пространственными даннымиРассмотрим запрос, выбирающий из базы данных страны, попавшие в заданный прямоугольник, и название которых начинается на букву "С". Прямоугольник задан полигоном (WKT-представление): POLYGON ((40 -28, 40 30, 5 30, 5 -28, 40 -28)).
Этот код скомпилируется, но работать не будет. Во время выполнения, на строке 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. Хранимые процедурыДля выборки геометрических фигур по критерию попадания в заданный прямоугольник для одной конкретной таблицы, достаточно создать следующую простую хранимую процедуру.
Входной параметр - это прямоугольник (заданный полигоном) в WKB-формате. В строке 8 он преобразуется статическим методом STGeomFromWKB в объект типа данных geography и уже на нём вызывается функция STIntersects, осуществляющая проверку на попадание конкретной границы в прямоугольник. В программе, в классе, реализующем DataContext (у нас этот класс называется ExampleDatabase), опишем обёртку для вызова этой процедуры.
Здесь, также как и для таблиц, описываются атрибуты для функции и параметров. В строке 4 вызывается хранимая процедура и результат сохраняется в execResult, затем, в строке 5, он преобразуется к требуемому типу данных и возвращается в основную программу. Пользуемся этой "радостью" следующим образом:
Замечание, если у вас, как у меня в рабочем проекте, несколько таблиц с географическими данными, то имеются следующие варианты.
Хранимые процедуры - это хорошо, но при использовании в LINQ to SQL, в описанной манере, у них есть один существенный недостаток: хранимые процедуры исполняются сразу и с сервера на клиент пересылаются все, попавшие в заданный регион, страны и уже потом над этим массивом выполняется дополнительная фильтрация. Т.е. трансляции в SQL всего LINQ-выражения не происходит. Для ухода от этой проблемы мы можем использовать inline-функции SQL Server'а. Table-valued функцииTable-valued функция, извлекающая из таблицы базы данных записи по критерию попадания в заданный регион, может быть создана следующим образом.
Т.е. по содержанию полностью аналогично хранимой процедуре, описанной раньше. Для функции также нужно будет создать свою обёртку.
В атрибуте метода указываем свойство IsComposable, которое говорит, что сейчас мы будем запускать функцию на SQL Server, а не хранимую процедуру. Для вызова функции используется метод CreateMethodCallQuery. Смотрим пример.
Результат тот же, что и при использовании хранимой процедуры. И в отладке видим прекрасную картину (всё linq-выражение было транслировано в sql-запрос): Всё, мне больше нечего сказать народу. |