четверг, 31 декабря 2009 г.

Обзор высокопроизводительных SAIL-решений

Около полугода назад, я уже сравнивал семантические фреймворки и ризонеры. Если на том этапе для меня важнее был выбор удобного фреймворка, нежели производительность, безопасность и т. д. (и выбор пал на OWLAPI+Pellet), то сейчас нашему проекту нужен высокопроизводительный ризонер с хранилищем в виде базы данных. Посему предлагаю рассмотреть и по возможности сравнить доступные на рынке SAIL-решения (Storage And Inference Layer).

Разделю хранилища на две группы: независимые (назовем их standalone) и являющиеся частью комплексных семантико-ориентированных enterprise-систем (для краткости дальше они будут именоваться PESSS-хранилища - Part of Enterprise Semantic System Store).

Сначала рассмотрим первую группу.

Standalone-хранилища

AllegroGraph

Официальный сайт: http://www.franz.com/agraph/allegrograph/
Цена: Бесплатно до 50 миллионов триплетов. При превышении лимита: Developer Edition: $4195 за процессор, Enterprise Edition: $34995 за процессор; академическая скидка - 50%. RacerPro продается отдельно; стоимость его лицензии можно узнать на официальном сайте (цены примерно того же порядка).
Поддерживаемые фреймворки: Jena, Sesame, Clojure
Архитектура:
Описание:
AllegroGraph разрабатывает фирма Franz Inc., которая широко известна благодаря таким продуктам, как TopBraid Composer (редактор онтологий), RacerPro (OWL DL ризонер) и др.

Поддерживает 2 основных типа вывода:
1. RDFS++
Уж как только не называют "RDFS плюс немного OWL"... На сей раз имеются в виду следующие предикаты: rdf:type, rdfs:subClassOf, rdfs:range, rdfs:domain, rdfs:subpropertyof, owl:sameAs, owl:inverseOf, owl:TransitiveProperty, owl:hasValue.
2. RacerPro
AllegroGraph интегрирован с еще одним ризонером Franz Inc. - RacerPro, который имеет выразительность OWL DL. Обсуждение OWL DL ризонеров в некоторой степени выходит за рамки сегодняшней статьи (ибо высокопроизводительными их пока назвать сложно), однако мы еще коснемся RacerPro при обсуждении PelletDb.

Итак, AllegroGraph является RDF-хранилищем с поддержкой двух ризонеров с полярными характеристиками. Впечатляет.

Franz Inc. также разрабатывает проект AGWebView, который, как несложно догадаться из названия, представляет собой Web-интерфейс для AllegroGraph.

BigOWLIM
Официальный сайт: http://www.ontotext.com/owlim/big/
Цена: 700 евро за каждое процессорное ядро
Поддерживаемые фреймворки: Sesame
Архитектура: Реализует SAIL для Sesame
Описание:
BigOWLIM имеет выразительность "RDFS with most of OWL Lite" (здесь подробнее), которая чуть шире выразительности ризонера RDFS++ AllegroGraph.

Разработчики OWLIM не скромничают и заявляют, что "BigOWLIM is the most scalable and efficient RDF Database!"

Интересно было бы сравнить производительность BigOWLIM с AllegroGraph RDFS++, но сделать это не так просто, ибо ризонеры базируются на разных принципах: static и dynamic materialization соответственно. Разобраться в различиях поможет описание RDFS++ dynamic materialzation (кстати, в видео идет сравнение с OWLIM). Итак, в теории AllegroGraph RDFS++ должен
1. быстрее загружать онтологии (ибо при этом ему не нужно делать вывод)
2. абсолютно безболезненно переживать модификацию онтологии
3. дольше выполнять запросы (ибо каждый запрос требует выполнения логического вывода).

От теории перейдем к практике и сравним результаты теста LUBM(8k) (информация взята отсюда и отсюда) при использовании сопоставимых конфигураций:
  • BigOWLIM: Core i7 940 (2.93GHz, quad-core, HT), 12GB (DDR3), 3x320GB 7.2krpm, RAID 0

  • AllegroGraph: Quad 1.8GHz AMD64 Opteron 884, with 16Gb of memory running Fedora 8

Все с ног на голову.

AllegroGraph показал в два раза худшее время загрузки (и это при том, что BigOWLIM во время загрузки еще и вывод делает). Так как у тестовой конфигурации AllegroGraph отсутствовала информация о дисковой подсистеме, пришлось восполнить этот пробел при помощи службы поддержки. Оказалось, использовались 2 обычных Sata диска, не объединенных в RAID (что равносильно одному диску). Если учесть преимущество на этапе загрузки в скорости тройного RAID 0 (используемого для тестирования BigOWLIM) против обычного диска, все встает на свои места.

А вот объяснить четырехкратное преимущество AllegroGraph при выполнении запросов (особенно при том, что теоретически на этом этапе он должен проигрывать BigOWLIM), пожалуй, можно лишь тем, что у AllegroGraph отлично реализованы алгоритмы вывода и поиска.

Подведем итоги локального сравнения.

В теории все звучит предельно ясно: чем более динамично меняется онтология, тем лучше себя показывает dynamic materialization; и наоборот. На практике же не все так однозначно: многое решают нюансы реализации.

Несмотря на то, что AllegroGraph производит впечатление гораздо более мощной, целостной и функциональной системы, BigOWLIM определенно достоин внимания при выборе высокопроизводительного RDF-хранилища (особенно учитывая колоссальную разницу в стоимости лицензий).

PelletDb

Официальный сайт: http://clarkparsia.com/pelletdb
Цена:
EULA-лицензия на PelletDb - 2495$ + лицензия на Pellet (если ваш проект распространяется по GPL, Pellet бесплатен) EULA 4995$ за сервер + лицензия Oracle Enterprise.
Поддерживаемые фреймворки: Jena
Архитектура: Взаимодействует с Oracle Enterpise при помощи Jena Oracle Adaptor
Описание:
Сомневался, в какую категорию поместить PelletDb. С одной стороны, что может быть "более enterprise", чем Oracle, с другой, - категория PESSS подразумевает семантико-ориентированность enterprise-системы. А в Oracle реализовано лишь хранилище с ризонером да SPARQL-endpoint. Я посчитал, что этого недостаточно для попадания в категорию PESSS. Кроме того, при таком раскладе в обоих категориях по три продукта :) В любом случае, это не принципиально.

Совсем "свеженький" продукт (релиз состоялся 1 декабря текущего года), реализующий для Oralce Enterprise логический вывод с выразительностью OWL DL.

Работает PelletDb следующим образом:
1. При помощи Jena Oracle Adaptor загружает из Oralce триплеты
2. Делает полный вывод
3. Загружает выведенные триплеты обратно в Oracle

Вырисовываются два существенных минуса такого подхода:
1. Для вывода вся модель должна быть в памяти. Это существенно снижает целесообразность использования PelletDb в enterpise-проектах (а жесткая привязка к Oracle Enterprise диктует использование PelletDb именно в этом секторе). Конечно же, разработчики учли этот "нюанс", поэтому PelletDb поддерживает (подробности в pelletdb whitepaper) режим вывода только схемы (=TBox). При этом в результате мы получим ABox, выведенный ризонером Oracle, и TBox, выведенный PelletDb.
2. Если "классический" Pellet делает вывод динамически, то PelletDb, очевидно, делает его статически. Полный OWL DL вывод - это очень медленно. Здесь бы пригодился динамический вывод... но используемая архитектура его не предполагает. В этом отношении интереснее смотрится связка RacerPro (OWL DL ризонер, который мы упоминали выше) и AllegroGraph. Ведь это продукты одной фирмы, а значит у них больше гибкости в вопросе интеграции.

<Лирическое отсупление на тему RacerPro>

В RacerPro можно выделить два режима работы.

В первом режиме происходит inmemory-вывод, который влечет за собой ограничение на размер ABox (аналогичен первому режиму PelletDb). В качестве решения можно отказаться от вывода ABox при помощи RacerPro, положившись на RDFS++ ризонер AllegroGraph (аналогично второму режиму PelletDb). Есть еще альтернативное решение, которое предполагает разбиение ABox на независимые части, которые выводились бы отдельно друг от друга. Пока это можно сделать только вручную, но уже в будущей версии для этого будет специальный API, а в более далекой перспективе обещают реализовать автоматический алгоритм разбиения ABox.

Второй режим не требует загрузки всей модели в память. Правда есть серьезные ограничения: нужно либо смириться с возможной неполнотой вывода, либо с тем, что для полного вывода RacerPro придется при каждом запросе обрабатывать всю базу.

</Лирическое отступление на тему RacerPro>

В целом PelletDb выглядит слишком узконаправленным: он заточен не только под Oracle Enterprise, но еще, по сути, под вывод лишь TBox. А ведь стоит недешево. Хотя, с другой стороны, "Everything is better with Bluetooth Oracle" :)

PESSS-хранилища

Virtuoso
Официальный сайт: virtuoso.openlinksw.com
Цена: Virtuoso Open Source Edition бесплатна. На нее распространяется ограничение соединения с внешними базами данных, отсутствие поддержки репликации и Virtual Database. Цену на подходящую вам конфигурацию коммерческой версии можно узнать на официальном сайте, выбрав Virtuoso Universal Server.
Поддерживаемые фреймворки: Jena, Sesame, Redland, Linq2Rdf, dotnetRDF
Архитектура:

Описание:
Virtuoso - очень мощный Enterprise-продукт фирмы OpenLink, которая специализируется на разработке RDBMS Middleware.

В состав Virtuoso входит одно из самых производительных и масштабируемых (например, сейчас в этом LOD-хранилище числится более 9 гигаквад) RDF-хранилищ, снабженных изощренной SPARQL-endpoint с поддержкой RDF Views. RDF Views - это реализация RDBMS2RDF, позволяющая динамически работать с реляционными данными через SPARQL (незаменимая возможность для проектов, ориентированных на интеграцию данных).

Выразительность встроенного ризонера довольно скромная: поддерживаются rdfs:subClassOf, rdfs:subPropertyOf, owl:equivalentClass, owl:equivalentProperty и owl:sameAs.

Отдельного упоминания стоит обилие качественной документации по Virtuoso.

OpenAnzo

Официальный сайт: http://www.openanzo.org/
Цена: Бесплатно (Eclipse Public License -v 1.0)
Описание:
OpenAnzo представляет собой RDF-хранилище с широким спектром семантического middleware, которое включает поддержку распределенных сервисов, работу в оффлайн, версионирование, ограничение доступа и т. д.

Одна из концепций данного проекта - ориентация на сервисы. Так, например, упомянутое выше RDF-хранилище, именуемое Anzo Store, не является хранилищем в привычном понимании этого слова. Anzo Store - это data-source-сервис, реализующий хранение триплетов в реляционных СУБД. Другой пример использования сервисов - синхронизация информации, реализованная при помощи сервисов репликации и уведомлений.

Встроенный ризонер отсутствует. В качестве решения этой проблемы предлагается выводить триплеты при помощи внешнего ризонера (а актуализировать информацию при помощи упомянутых выше сервисов репликации и уведомлений) или встроить вызов ризонера в обработчик SPARQL-запросов.

Предоставляется клиентский API для Java, .NET и JavaScript.

В целом, очень интересный Open Source проект с широким enterprise-функционалом.

Semantics.Server

Официальный сайт: http://www.intellidimension.com/products/semantics-server/
Цена: В зависимости от версии: например, Standard Edition License - 10000$, Enterprise Edition License - 15000$ + лицензия Microsoft SQL Server.
Описание:
Одна из немногих семантических разработок на .NET. Причем не просто на .NET, а в тесном сотрудничестве с Microsoft.

Semantics.Server представляет собой целый ряд средств, включая свой фреймворк, OWL Full ризонер и хранилище на базе SQL Server. Особенно хотелось бы отметить следующие возможности:
1. Объединение реляционных и семантических данных при помощи T-SQL (в отличие от Virtuoso RDF Views, другие СУБД не поддерживаются)
2. Распределение одного RDF-графа на множество серверов.

В maillist'е Semantics.Server периодически появляются сообщения о низкой производительности продукта по сравнению с конкурирующими системами. Публичных тестов практически нет, но подобные отзывы полностью коррелируют с моим опытом использования Semantics.Server. Поэтому перед тем, как воспользоваться возможностью распределения RDF-графа на множество серверов, стоит проверить: может выйти так, что конкурирующий продукт решит поставленную задачу в заданные сроки даже на одном сервере.

Также серьезным недостатком Semantics.Server является ограниченная поддержка других (помимо SQL Server) СУБД. Хотя работа в этом направлении ведется:
"Yes, it only works with SQL Server at the moment using out of the box components (you could write a custom datasource component to connect to any external source of data). We do expect to support access to additional relational stores using our SQL Datasource, but have no timeline for that right now."

Заключение

Пожалуй откажусь от идеи делать выводы уровня must live/must die касательно вышеописанных продуктов: уж слишком они все разные.

В обзоре были рассмотрены далеко не все существующие продукты. Буду рад увидеть в комментариях дополнение этого поста.

воскресенье, 4 октября 2009 г.

LINQ. First vs. Single. Часть вторая.

Неожиданно предыдущий пост про LINQ породил несколько вопросов, которые в свою очередь породили еще несколько :) О них-то мы сегодня и поговорим.

Основная мысль того поста была настолько очевидной, что я даже сомневался, писать или нет. Когда же пост уже был написан, решил сделать его хоть немного менее унылым, снабдив исходниками реализаций методов First и Single LINQ To Objects. При этом реализация метода Single мне показалась неоптимальной. За комментариями по этому поводу я обратился на форумы MSDN. К сожалению, я не сразу понял мысль коллег про "exceptional cases"... но в целом дискуссия получилась довольно конструктивной.

Итак, напомню, как выглядел код, полученный при помощи Reflector'a:

TSource local = default(TSource);
long num = 0L;
foreach (TSource local2 in source)
{
if (predicate(local2))
{
local = local2;
num += 1L;
}
}
long num2 = num;
if ((num2 <= 1L) && (num2 >= 0L))
{
switch (((int) num2))
{
case 0:
throw Error.NoMatch();

case 1:
return local;
}
}
throw Error.MoreThanOneMatch();

А почему бы не добавить в тело условия if (predicate(local2)) проверку на равенство num двум:

TSource local = default(TSource);
long num = 0L;
foreach (TSource local2 in source)
{
if (predicate(local2))
{
local = local2;
num += 1L;
if (num == 2)
throw Error.MoreThanOneMatch();
}
}
long num2 = num;
if ((num2 <= 1L) && (num2 >= 0L))
{
switch (((int) num2))
{
case 0:
throw Error.NoMatch();

case 1:
return local;
}
}

С одной стороны, этот код привносит условие, которое тоже требует машинного времени для своего выполнения. Но, во-первых, условие if (num == 2) - элементарное, а во-вторых, при любом раскладе оно выполнится не более двух раз. Что же мы при этом экономим? Экономим мы k проходов Enumenator'а (который лежит в основе foreach) и столько же выполнений предиката (только в случае наличия дубликатов, ибо если элемент уникальный, нам все равно придется пройти коллекцию полностью). Очевидно, что стандартная реализация проигрывает.

В принципе у текущей реализации есть оправдание. Вызывая метод Single, мы заранее предполагаем, что в коллекции будет находиться один подходящий элемент (иначе воспользовались бы методом First), поэтому данная реализация проигрывает лишь в редких случаях, отсрачивая генерацию исключения. Сюда же можно добавить, что цена генерации исключения в большинстве случаев будет гораздо выше издержек алгоритма.

В целом, конечно, данный вопрос представляет собой, в большей степени, академический интерес (кстати, неплохая иллюстрация закона дырявых абстракций): на практике в 99.9% случаев проблем из-за текущей реализации метода Single не будет. Если же для Вас метод Single стал проблемой, замените его на собственный extension method.

На этом с LINQ To Objects на сегодня закончим - поговорим о LINQ To Entities и его реализации метода Single. Напомню, что в EF v1 метод Single не поддерживался: приходилось либо довольствоваться First, либо использовать разные костыли, например, вот этот. Проблема приведенного по ссылке workaround'а в том, что при вызове query.Count из базы будут возвращены все удовлетворяющие запросу записи (в том числе и дублирующиеся). В случае, если мы обращаемся к "тяжелым" данным, результат может быть плачевным.

Так как же с Single обстоят дела в EF 4? Добравшись до VS 2010, я был приятно удивлен - работает. Но маленький параноик в моей голове убедил меня (спасибо ему за это) открыть Profiler и посмотреть на генерируемый SQL-код. А вот и он:

SELECT TOP (2)
[Extent1].[AddressId] AS [AddressId],
[Extent1].[City] AS [City],
[Extent1].[Street] AS [Street],
[Extent1].[PostalCode] AS [PostalCode],
[Extent1].[Apartment] AS [Apartment]
FROM [dbo].[Addresses] AS [Extent1]
WHERE 1 = [Extent1].[AddressId]

Данный код при наличии дубликатов возвращает две записи. Это, конечно, чуть лучше, чем решение, обсуждавшееся выше (там возвращалось неограниченное число дубликатов), но хотелось бы большего: хотелось бы, чтобы вопрос наличия дубликатов решался на стороне СУБД (например, при помощи подзапросов), а возвращалась бы либо одна (уникальная) запись, либо вообще ничего (если есть дубликаты).

За комментариями я в очередной раз обратился на форумы MSDN (прекрасное место :) ). Прежде всего, меня интересовало, финальное ли это решение для EF 4. Как Вы можете судить по ответу, шансов на то, что это поведение изменится до релиза .NET 4, мало.

На этой печальной ноте и закончим сегодняшний разговор о LINQ.

суббота, 3 октября 2009 г.

Знакомимся с IKVM.NET

В статье "Сравнительный анализ фреймворков для работы с онтологиями под .NET и Java" я уже упоминал проект IKVM.NET. Cегодня я хотел бы поговорить о нем подробнее.

IKVM.NET предоставляет поддержку Java для Mono и .NET, включает в себя JVM, рализованную на .NET, а также interoperability-средства между Java и .NET (с акцентом на использование Java в .NET, а не наоборот).

3 основных варианта использования IKVM.NET:

1. Drop-in JVM (ikvm.exe): Позволяет динамически запускать Java приложения под .NET.
2. Use Java libraries in your .NET applications (ikvmc.exe): Статически компилирует jar'ы в .NET-сборки.
3. Develop .NET applications in Java (ikvmstub.exe): Позволяет использовать .NET-классы при разработке на Java (при условии, что затем Java-код будет скомпилирован в .NET при помощи ikvmc.exe).

Несмотря на то, что текущая версия проекта "всего лишь" 0.40, он уже очень многое умеет. Самое узкое место - графический интерфейс: конвертация AWT и Swing в WinForms (ни о Java FX, ни о WPF пока ни слова) - очень трудоемкая задача. К тому же, по заявлению разработчиков, приоритет этой задачи низкий (хотя, насколько я могу судить по их блогу, в последнее время деятельность в этом направлении существенно активизировалась). Полностью разделяю позицию разработчиков относительно GUI и считаю гораздо более ценной миссию "портирования" бесчисленного множества Java-библиотек в .NET. В конце концов, имея backend, всегда можно прикрутить какой-никакой интерфейс, а вот наоборот - вряд ли.

Быстродействие и потребление памяти IKVM.NET можно измерить, как минимум, двумя способоами: сравнить показатели 1. "чистой" Java и конвертированной под .NET 2. конвертированного под .NET кода и "чистого" .NET кода. В первом варианте используются разные платформы, поэтому многое зависит от деталей реализации (чего только различия в сборщиках мусора стоят). А вот второй вариант более объективен (при как можно близких реализациях тестовых программ под Java и .NET). По заверениям разработчиков, быстродействие (вероятно, использовался второй подход) может падать в 1.5-2 раза... хотя на форумах я встречал и свидетельства об ускорении работы приложений (видимо, здесь для сравнения использовался первый подход). Вообще при перекомпиляции происходит много интересных метаморфоз: так, например, мне приходилось сталкиваться с тем, что не работающий при некоторых условиях Java-код отлично работал после компиляции в .NET.

А не костыль ли это? В определенной степени - да. Первое, что бросается в глаза при начале использования конвертированных Java-классов, - несоответствие code agreement'ов в Java и .NET. Из-за этого код становится немного "грязнее". Но это мелочь по сравнению, например, с тем фактом, что в любой момент Вы можете натолкнуться на критичную для Вас возможность, которая еще не реализована. Но, с другой стороны, а какие есть альтернативы? Использовать другие interoperability-средства (возможно, еще менее надежные) или переписывать необходимый Java-фреймворк под .NET? Сложный выбор. Если Вам потребуется функциональность IKVM.NET, тщательно взвесьте все за и против, ибо последствия принятого решения могут оказаться очень серьезными. Я свой выбор сделал (по крайней мере для текущего проекта) и пока не разочаровался.

С огромным уважением отношусь к разработчикам такого технического сложного проекта. It is really rocket science! Заставить две разные платформы (которые похожи лишь с высоты орлиного полета) работать вместе - это дорогого стоит.

Я хотел было материализовать свою благодарность разработчикам в виде "пожертвования"... но не нашел традиционной кнопки Donate на официальном сайте. Списавшись с разработчиками, получил ответ:

Hi Alexander,

A donation is not necessary, but your mail is appreciated!

Regards,
Jeroen

Вот так.

На этом знакомство с IKVM.NET будем считать оконченным. Если возникнет интерес, в следующий раз рассмотрим применение IKVM.NET на практике.

пятница, 2 октября 2009 г.

LINQ. First vs. Single

Пролог: Некоторым читателям изложенные во вводной части поста мысли могут показаться слишком очевидными и лежащими на поверхности. Однако идея поста возникла в ходе рефакторинга чужого кода, а раз есть прецедент - об этом стоит поговорить.

Для получения первого элемента, удовлетворяющего предикату, в LINQ используются два основных метода: First и Single. В различных реализациях LINQ детали могут меняться, поэтому для определенности рассмотрим логику этих методов на примере LINQ To Objects: First просто возвращает первый элемент (а если его нет - генерирует исключение), а Single возвращает единственный элемент (а исключение генерирует не только если элементов нет, но и если их больше одного). Однако несмотря на то, что названия методов вполне отражают различия между ними, порой программисты относятся к этим двум методам как к равноценным и взаимозаменяемым.

Давайте немного углубимся в алгоритмы работы методов: First находит первый элемент - и возвращает его, Single же на этом не останавливается и продолжает поиск второго элемента, дабы либо убедиться, что его нет, либо сгенерировать исключение, если он все же есть. Как следствие, First всегда быстрее Single. Таким образом, для получения первого элемента всегда нужно использовать First, Single же пригодится лишь в тех случаях, когда необходимо быть уверенным, что полученное значение является единственным (как бы банально это ни звучало, порой об этом забывают).

Вооружимся Reflector'ом и от теории перейдем к практике.

Для начала убедимся, что методы First и Single работают именно по описанным выше алгоритмам. Для этого рассмотрим выдержки кода метода First:

foreach (TSource local in source)
{
if (predicate(local))
{
return local;
}
}
throw Error.NoMatch();

и Single:

TSource local = default(TSource);
long num = 0L;
foreach (TSource local2 in source)
{
if (predicate(local2))
{
local = local2;
num += 1L;
}
}
long num2 = num;
if ((num2 <= 1L) && (num2 >= 0L))
{
switch (((int) num2))
{
case 0:
throw Error.NoMatch();

case 1:
return local;
}
}
throw Error.MoreThanOneMatch();

Если с методом First все ясно, то к реализации метода Single у меня есть вопросы. В ходе цикла num не проверяется на равенство двум, поэтому выполнение цикла в любом случае продолжается до конца коллекции. Сразу представляется параноидальная ситуация, когда Single'ом с "тяжелым" предикатом обрабатывается огромная коллекция с объектами с "тяжелой" перегрузкой Equals, и все ужасно тормозит... Если в комментариях не последует оперативного разъяснения (может, я чего проглядел, а может, это известная "фича"), обращусь за оным в MSDN'овские форумы.

В заключение скажу пару слов об Entity Framework, а точнее о LINQ To Entities.

В EF v. 1 LINQ To Entities не поддерживал метод Single. Особенно неожиданным это открытие становилось для тех, кто переходил с, казалось бы, менее функционального LINQ To SQL: EF оказался заложником своей универсальности (по сравнению с заточенным под одну СУБД LINQ To SQL). Хотя на практике горевать по этому поводу приходилось редко (особенно на фоне более "выдающихся" ограничений EF v. 1), потому что, как правило, такие запросы выполняются по первичному ключу, уникальность которого гарантируется на уровне БД. Сейчас нет под рукой VS 2010... но как доберусь до нее - проверю, реализовали ли эту возможность (соответствующие планы у команды EF были) в текущем public-build'е EF4 и обновлю пост.

Update. Продолжение в этом посте.

среда, 23 сентября 2009 г.

Пустите меня в Интернет!

Так вышло, что последние несколько дней я провел в Китае (всего несколько часов как на Родине). При заселении в гостиницу на ресепшине сказали, что Интернета у них нет, чем меня очень удивили. Поднимаюсь в номер на 12-й этаж - ничего подобного: стабильно ловится 3-4 беспроводных соединения (причем два из них незащищенные). Защищенные соединения я брать во внимание не стал: взламывать WEP/WPA не было ни времени, ни желания. Да и черт знает, может, у них за эту безобидную шалость смертная казнь полагается :)

Одно незащищенное соединение сразу привлекло мое внимание, ибо называлось оно TP-LINK. TP-LINK - производитель дешевого (по сравнению с Cisco) сетевого оборудования с соответствующим качеством (по крайней Wi-Fi адаптером их производства, "счастливым" обладателем которого я одно время являлся, я остался недоволен - пришлось вернуть по гарантии). Я это к тому, что была высокая вероятность, что это именно домашняя сеть с расшаренным по незнанию (а, может, и по доброте душевной) соединением. Так и вышло. Правда интернет был не ахти (вечером вообще казалось, что через эту вайфайку сидит полкитая), да и сигнал был слабеньким и периодически пропадал. Это заставило меня взглянуть на второе соединение.

Со вторым незащищенным соединением все было гораздо интереснее. После подключения при попытке посещения любого сайта происходил редирект на сайт провайдера. Вкратце, предлагалось два типа соединения. Первое - Ethernet-подключение (причем выдаваемые логин и пароль позволяли подключаться и к Wi-Fi, чем я впоследствии и воспользовался). Второе - более мобильное решение, ориентированное, как я понимаю, на туристов. Покупаешь сим-карту одного из местных операторов, звонишь в техподдержку, региструешься, после чего твой телефон выступает в роли электронного кошелька для Wi-Fi (это то, как я сей процесс понял, - проверить не довелось).

Я уж было собрался идти за сим-картой... но тут мне на глаза попалась надпись, гордо провозглашавшая количество пользователей (точную цифру не помню, но счет был на десятки тысяч). Тут я понял, что теория вероятности сегодня на моей стороне. Глаза загорелись - уже и забыл, что собирался за симкой. Итак, выбираем первый способ подключения (схема авторизации там немного необычная - через ActiveX-компонент, но это не суть важно) и пытаемся подобрать пароль (помня огромное число абонентов). Так... Какие самые популярные логины и пароли? (задача усложняется тем, что нужно подобрать не только пароль, как это обычно бывает, а пару логин-пароль). Пробую:
1. Логин: qwerty Пароль: qwerty - мимо
2. Логин: qwery Пароль: 12345 - Successfully connected
Со второго раза - вот так брутфорс :)

Из гуманистических соображений каждый день я подбирал новый логин с паролем (благо это было несложно), тем самым стараясь сильно не разорять каждого отдельного пользователя. Надеюсь, они на меня не в обиде :) Я бы с удовольствием заплатил... но просто не нашел, как это сделать: информация на английском очень скупая, а иероглифы мне мало чем помогли.

Мораль: Не используйте стандартные логины и пароли. Пусть уж лучше они будут несложные - главное, чтобы свои, родные, с намеком на уникальность.

P. S. Хотел написать этот пост еще в Китае (как полагается, со скринами)... но меня упорно не пускали ни на blogger.com, ни на blogspot.com (ни на их поддомены). Видимо, это проделки Великого Китайского Фаервола. Попытался воспользоваться анонимайзером - не пускает. Наверное, нужно было найти свеженькую анонимную проксю... но я поленился.

PP. S. Китайцы - красавцы: трудолюбивые, чертяги.

вторник, 15 сентября 2009 г.

Правим баги. Подсознательно

Попросил меня сегодня коллега помочь разобраться с багом. Ничто не предвещало серьезных проблем (хотя чужой код - всегда потемки). Однако наскоком решить проблему не удалось. По прошествии двух часов, я уже перепробовал все мыслимые-немыслимые варианты, нагуглил целый ворох подобных топиков/постов - все не то. Попробовал отвлечься ненадолго - результат тот же. Сидел уже в каком-то отрешенном состоянии (думаю, каждый программист бывал в подобной ситуации, когда от кажущейся безысходности руки опускаются и голова отказывается искать новые варианты). И тут неожиданно в голову пришла мысль: "А не убрать ли мне этот using (оставив, конечно же, сам код внутри блока)" (речь об операторе using в C#, который после выхода из блока вызывает метод Dispose). Мысль возникла изниоткуда: я не пришел к ней логическим путем - просто неожиданно мне захотелось убрать using и посмотреть, что будет. Все еще находясь в "анабиозе", я запустил приложение - к моему удивлениею, все заработало. После этого разобраться, в чем же была истинная причина ошибки, было делом техники.

Встает вопрос, чем же была эта возникшая внезапно мысль: случайностью, провидением или, может, результатом работы подсознания, которое отдельно от сознания прорабатывало варианты? :)

P. S. Не секрет, что подобным образом свершалось немало открытий... но приятно иногда прочувствовать это на себе.

PP. S. С какой легкостью после этого закрывается море вкладок в браузере, которые, как казалось, могли помочь...

вторник, 4 августа 2009 г.

Перевод книги "Semantic Web Programming"

Около полугода назад я пытался добиться перевода книги "SEMANTIC WEB for the WORKING ONTOLOGIST" (об этом я писал здесь и здесь). Насколько мне известно, процесс заглох на этапе переговоров между издательствами.

На этот раз сложилась обратная ситуация: к семантическому сообществу с инициативой публикации перевода одной из книг по тематике Semantic Web обратился Виктор Штонда, генеральный директор издательства "Диалектика". В блоге Виктора Вы можете найти пост с обсуждением данного вопроса. Так что если Вас интересует перевод, - не стесняйтесь, присоединяйтесь к обсуждению :)

P. S. Название поста обусловлено тем, что на данный момент наиболее вероятен перевод именно книги "Semantic Web Programming" (о которой я, кстати, недавно писал).

пятница, 10 июля 2009 г.

PRO-ключ к плагину Huagati DBML/EDMX Tools в подарок

Вчера мне на gmail пришло сообщение от Huagati Systems Co., Ltd. с ключом для плагина Huagati DBML/EDMX Tools (о котором я писал ранее). Судя по содержанию письма, это был ключ на PRO-версию плагина (стоимостью $119.95). Промелькнула мысль, что это благодарность за обзор продукта (хотя верилось с трудом). Когда же я в следующий раз посетил Google Analytics, увидел, что на мой пост ссылается в своем твиттере один из сотрудников Huagati Systems. Тут уж я не выдержал - написал в саппорт с целью узнать, не по ошибке ли мне был выслан ключ. Оказалось, действительно, серийник был выслан в знак благодарности за обзор (хотя рекламных целей я не преследовал; честно-честно :) ).

К чему все это? Дело в том, что мне этот ключ без надобности, и я с удовольствием подарю его (с разрешения Huagati Systems) первому попросившему (пожалуйста, пишите только если Вам действительно нужна Pro-лицензия). Напомню, что PRO-версия, во-первых, снимает ограничение на количество сущностей в модели, а во-вторых, содержит сборку, которая позволяет использовать функционал плагина в runtime.

понедельник, 6 июля 2009 г.

Обзор книги "Semantic Web Programming"

Некоторое время назад в моих руках оказалась книга Semantic Web Programming (за что большое спасибо моему научному руководителю). На днях я ее дочитал - спешу поделиться впечатлением.

"Semantic Web Programming" - первая на моей памяти книга, которая, во-первых, рассматривает так много языков, средств и технологий Semantic Web, а во-вторых, целиком ориентирована на практическое их применение. На мой взгляд, это делает данную книгу незаменимым чтивом (в первую очередь, для новичков Semantic Web): такая подборка материалов дорогого стоит.

Книга была издана в середине апреля этого года, поэтому информация в ней предельно актуальна (и это не может не радовать).

Содержание книги условно разбито на четыре части.

Первая (главы 1-2) "Introducing Semantic Web Programming" и вторая (главы 3-7) "Foundations of Semantic Web Programming" части носят теоретический характер (хотя и здесь авторы стараются приводить примеры, не давая читателю забыть о практической направленности книги). Первая часть предлагает читателю вводный обзор Semantic Web, а вторая рассматривает основные языки и технологии (RDF, OWL, SPARQL, SWRL). Опытные деятели Semantic Web могут смело пропустить эти две части, сделав исключение лишь для 5-й главы (в которой рассматриваются семантические фреймворки, их компоненты, а также примеры реализаций).

Третья часть "Building Semantic Web Applications" (главы 8-11) рассматривает подходы к использованию Semantic Web в реальных приложениях. Глава 8 представляет собой обзор семантического фреймворка Jena (в книге основной акцент сделан именно на этот фреймворк). Последующие 3 главы (9, 10 и 11) описывают три очень важные проблемы (и способы их решения): combining information (рассматривается доступ к разнородным источникам данных, преобразование XML в RDF, доступ к реляционным СУБД при помощи SPARQL), aligning information (описывается объединение онтологий при помощи OWL, SWRL, XSLT, RDFS и кода) и sharing information (разговор идет о микроформатах, eRDF и RDFa, который в виду последних событий стал особенно актуален).

Четвертая часть "Expanding Semantic Web Programming" (главы 12-15) раскрывает некоторые тонкие моменты (глава 12 - использование семантических сервисов; глава 13 - хранение в онтологиях пространственных и временных значений), подводит итоги (глава 14 - patterns and best practices) и описывает ближайшие перспективы (глава 15 - moving forward).

В целом, на мой взгляд, книга "Semantic Web Programming" просто обязана стать бестселлером (в круге интересующихся Semantic Web, естественно), и ее можно смело ставить в один ряд с "SEMANTIC WEB for the WORKING ONTOLOGIST". Очень радует появление в последнее время достойных книг по тематике Semantic Web. Такие книги снижают входной барьер и расширяют круг интересующихся данной тематикой, что в конечном счете способствует дальнейшему развитию Semantic Web.

вторник, 30 июня 2009 г.

Один день из жизни эникейщика

Никогда раньше мне не приходилось официально выполнять роль сисадмина... а тут подвернулась такая возможность. Знакомый, уходя в отпуск, попросил подменить его на месяц в охранной фирме (где он работает сисадмином). Я сначала не хотел соглашаться... но его убедительный тон и фразы вроде "Да там все настроено - тебе только проконтролировать нужно будет; работы минимум" меня убедили.

Назвать возложенные на меня обязанности системным администрированием у меня язык не поднимается... я, скорее, выполняю роль эникейщика с расширенным функционалом :)

Важный момент, которому я не придал (зря) значения: фирма буквально на днях переехала в новый офис.

И вот сегодня у меня было боевое крещение. Уже уезжая, знакомый вспомнил, что впопыхах забыл поднять TeamSpeak-сервер, при помощи которого планировалось с 1 июля запустить общение ведущих охранных фирм города (для взаимопомощи). К этому я был готов, и приступил сразу же. Какого же было мое удивление, когда:
  1. Внезапно упал один из интернет-каналов

  2. Из бухгалтерии поступило сообщение о том, что перестал работать интернет-банк
Этого я никак не ожидал.

В процессе обзвона охранных фирм с целью передать новые настройки TeamSpeak решались возникшие проблемы.

Упавший интернет
После продолжительного общения со службой поддержки провайдера на ADSL-модеме таки загорелся волшебный диод с гордой подписью "Internet". Однако охранные системы, подцепленные на этот канал, по-прежнему отказывались обновляться через GPRS и использовали для этого резервное средство - СМС. Обновление через СМС, во-первых, обходится дороже, а, во-вторых, поговаривают, частенько вызывает подвисание охранных систем (да-да, я тоже был удивлен, что в таких серьезных девайсах есть такие существенные баги). Неудивительно, что источником проблемы был брендмауэр. После приручения брендмауэра, обновление через GPRS заработало.

Интернет-банк
Эта проблема оказалась самой забавной. После переезда интернет-банк при аутентификации стал выдавать малоинформативную ошибку о неверности кодировки. Версия того, что причиной "поломки" явился переезд, была отброшена как антинаучная. Звонок администраторам интернет-банка не прибавил ясности. Истинная причина выяснилась совершенно случайно: оказалось, что закончился срок действия криптоключа. Чего греха таить, в тот момент я подумал очень плохо о разработчиках этой системы с крайне "интуитивными" сообщениями об ошибках.

Заключение
Конечно, проблемы, с которыми мне пришлось столкнуться, детские, и на ithappens.ru можно найти гораздо более увлекательные админские истории. Однако эти проблемы заставили меня крепко поволноваться... Надеюсь, что на этом мои приключения закончатся, и дальше все будет гладко. Уже совсем скоро узнаю, насколько я реалистичен в оценках :)

Подведем итог:
Мораль № 1. При необходимости воспользоваться услугами частной охранной фирмы подойдите к этому вопросу со всей серьезностью: не постесняйтесь напроситься посмотреть на организацию технической части. Конкуренция и, как следствие, демпинг порой творят "чудеса", и Вы можете быть очень неприятно удивлены увиденным. Думаю, в центральных регионах дела обстоят получше (хотя...), но, как говорится, "доверяй , но проверяй".

Мораль № 2. Не верьте людям :) Особенно системным администраторам :)

P. S. Антон Васильевич, передаю тебе пламенный привет. Очень пламенный :)

вторник, 23 июня 2009 г.

Вышел Entity Framework CTP 1

Вчера был анонсирован Entity Framework CTP 1, включающий следующие возможности:
1. Улучшенная поддержка N-tier
2. POCO template (для автоматической генерации POCO-cущностей по модели)
3. Поддеркжа Code only подхода (вдобавок к существующим Model First и Code First).

Скачать EF4 CTP 1

четверг, 18 июня 2009 г.

Entity Framework 4: Часть 1. Работа над ошибками

В предыдущей статье по EF4 я, не найдя возможностей кастомизации процесса генерации DDL, предположил, что они не включены в beta 1 и оказался неправ. Оказывается, этот вопрос уже освещен в MSDN.

понедельник, 8 июня 2009 г.

Entity Framework 4: Часть 1. Pluralization, генерация DDL и удаление сущностей в дизайнере

В предыдущих статьях по Entity Framework мне удавалось обходить недостатки EF v. 1 стороной. Видимо, делал я это не так искусно, как мне казалось :) , поэтому меня стали подозревать в необъективности. Действительно, Vote of No Confidence родился не на ровном месте (хотя более информативен не сам Vote of No Confidence, а ответ на него Tim Mallalieu) - EF v. 1 содержит целый ряд существенных недоработок: отсутствие поддержки POCO, ограниченный маппинг хранимых процедур, принудительный и безальтернативный маппинг вторичных ключей как ассоциаций и т. д. Хотя указанные ограничения можно обойти... серьезная ORM должна поддерживать эти возможности без ритуальных танцев (как, например, это делает NHibernate). Так почему же я не касался этих моментов раньше? Я посчитал, что обсуждение недостатков EF v. 1 будет гораздо интереснее в рамках обзора EF4 (ранее известного как EF v. 2): так мы сможем рассмотреть не только проблемы EF v. 1, но и их решения, предлагаемые EF4.

Итак, предварительно скачав Visual Studio 2010 beta 1 (в состав которой входит EF4 beta 1) я приступаю к написанию серии статей по EF4.

Для начала создадим новое консольное приложение и добавим в него новую модель для базы данных, которую мы использовали в цикле статей "Влюбляемся в Entiy Framework" (бэкап базы прикреплен к этому посту). Щелкаем правой кнопкой по имени проекта -> Add ->New Item...
Первое, что бросается в глаза, - наличие двух темплейтов, касающихся EF. ADO.NET. EntityObject Generator мы пока проигнорируем (вернемся к нему в следующей статье) и создадим модель, воспользовавшись знакомым нам из EF v. 1 темплейтом ADO.NET Entity Data Model.

На первом шаге дизайнера выбираем Generate From Database, а на втором - указываем строку соединения и задаем ей имя SecondModelEntities. Здесь все без изменений. На следующем шаге выбираем таблицы Addresses и Persons и устанавливаем чекбокс "Pluralize or singularize generated object names", о назначении которого мы поговорим далее:
Кликаем Finish - модель создана.

Pluralization/Singularization
Дизайнер LINQ To Sql c самой первой версии поддерживал замечательную возможность: он учитывал тот факт, что обычно таблицы в базе данных именуются во множественном числе (Users, Products, Customers), и поэтому при именовании сущностей и "единичных" сторон ассоциаций использовал "сингулязированные" (затрудняюсь лаконично перевести этот термин) имена (в единственном числе: User, Product, Customer). Мелочь, а приятно: избавляет от унылой рутинной работы (особенно при первоначаольном заполнении модели).

Разработчики EF пошли дальше.

Во-первых, EF поддерживает не только преобразование из множественного числа в единственное, но и обратное - pluralization (на тот случай, если у Вас сущности в базе именуются в единственном числе, что тоже не редкость).

Во-вторых, в отличие от LINQ To SQL, в EF Pluralization/Singularization (далее - P/S) можно отключить. Зачем? Если не брать в расчет мазохистские соображения, P/S мешает людям, именующим сущности базы данных не на английском языке (а стандартная реализация, вполне логично, поддерживает только его). Причем, если EF видит в имени сущности символ из другого языка, P/S не применяется впринципе (имя сущности в этом случае используется as is). Проблемы возникают, когда сущность именуется на другом языке, но при этом содержит только латинские символы: так, немецкое слово Kunden (нем. заказчики) будет "плюрализовано" до Kundens, хотя на самом деле должно было быть "сингуляризовано" до Kunde. Жалобы разработчиков, использующих LINQ To SQL (а там отключить Singularization нельзя), материализовались в возможность отключения P/S в EF. Кстати, в ситуации с не английскими языками EF предлагает и более конструктивный подход - написание собственного P/S-сервиса.

Итак, в-третьих, P/S-сервис в EF4 не вещь в себе, как в LINQ To SQL: он может быть расширен путем реализации наследников абстрактного класс PluralizationService. Возьмем за основу код из поста в блоге EF Design:

PluralizationService pluralizationService = PluralizationService.CreateService(new CultureInfo("en-US"));
ICustomPluralizationMapping mapping = pluralizationService as ICustomPluralizationMapping;
if (mapping != null)
mapping.AddWord("Kunde", "Kunden"); //Обратите внимание, что метод ICustomPluralizationMapping.Add в beta 1 был переименован в AddWord
Рассмотрим этот код подробнее.

Сначала вызывается статический метод PluralizationService.CreateService, который возвращает наследника PluralizationService. Убедимся в этом при помощи Reflector'а:

public static PluralizationService CreateService(CultureInfo culture)
{
EDesignUtil.CheckArgumentNull<cultureinfo>(culture, "culture");
if (culture.TwoLetterISOLanguageName != "en")
{
throw new NotImplementedException("We don't support locales other than english yet");
}
return new EnglishPluralizationService();
}
Как видим, передавать в параметре culture не английскую культуру не имеет смысла - будет сгенерировано исключение. А вот реализация Pluralization-сервиса для английского языка возложена на класс EnglishPluralizationService. С реализацией этого класса Вы можете ознакомиться при помощи Reflector'а... а мы же пойдем дальше.

После вызова метода CreateService мы добавляем новое исключение при помощи метода ICustomPluralizationMapping.AddWord. В приведенном выше посте в блоге EF Design добавляется исключение child-children. Сделано это, видимо, с целью демонстрации и не более: это исключение уже есть в реализации класса EnglishPluralizatonService. Мы же добавим исключение kunde-kunden. Зачем? Таким образом мы сэмитируем поддержку немецкого языка. Если Вам вдруг понадобится работать с базой, в которой сущности именуются не на английском языке, а реализовывать наследника PluralizationService в Ваши планы не входит (ведь это не самая простая и тривиальная задача), можно обойтись добавлением в исключения имен сущностей, имеющихся в Вашей базе. Однако, как я уже отмечал, это сработает лишь для имен, состоящих из латинских букв: если EnglishPluralizationService находит в имени нелатинский символ, он не применяет к нему никаких преобразований и возвращает его as is.

Итак, мы создали экземпляр PluralizationService и добавили в него свое исключение - теперь необходимо сгенерировать модель при помощи нашего экземпляра PluralizationService. В дизайнере жестко прошито создание модели при помощи EnglishPluralizationService со стандартным набором исключений, поэтому модель нам придется создавать вручную. О том, как вручную генерировать SSDL, CSDL, MSL и классы, отлично написано в этом блог-посте.

Для того чтобы поэкспериментировать с созданием EDM при помощи API EF, создадим модель ThirdModel и на первом шаге дизайнера выберем Empty Model. В базе данных создадим новую таблицу Kunden с единственным атрибутом KundeId. Также создадим консольное приложение, которое будет генерировать SSDL, CSDL и MSL-файлы. После получения этих файлов мы подставим их в пустую (пока) модель. Таким образом, использовав кастомизированный PluralizationService для генерации первичной модели, мы лишим себя "удовольствия" переименования множества сущностей и навигационных свойств.

В целом, данный эксперимент не имеет большого практического смысла. Он, скорее, нацелен на получение опыта использования низкоуровнего API EF (который нам пригодится в следующей статье).

Для начала напишем код создания файлов со схемами:

static void Main(string[] args)
{
string connectionString = "Data Source=DEVELOPER;Initial Catalog=FirstModel;Integrated Security=True;MultipleActiveResultSets=True";
string provider = "System.Data.SqlClient";

string conceptualSchemaNamespaceName = "ThirdModel";
string storeSchemaNamespaceName = conceptualSchemaNamespaceName + ".Store"; //Гарантирует отсутствие конфликтов имен между CSDL и SSDL

//генерируем SSDL
EntityStoreSchemaGenerator storeGenerator = new EntityStoreSchemaGenerator(provider, connectionString, storeSchemaNamespaceName);
storeGenerator.GenerateStoreMetadata();
storeGenerator.WriteStoreSchema(@"StoreSchema.ssdl");
EntityContainer storeEntityContainer = storeGenerator.EntityContainer;

//инициализируем кастомизированный PluralizationService
PluralizationService pluralizationService = PluralizationService.CreateService(new CultureInfo("en"));
ICustomPluralizationMapping mapping = pluralizationService as ICustomPluralizationMapping;
if (mapping != null)
mapping.AddWord("Kunde", "Kunden"); //Обратите внимание, что метод ICustomPluralizationMapping.Add в beta 1 был переименован в AddWord

//генерируем CSDL и MSL
string entityContainerName = "ThirdModelEntities";
EntityModelSchemaGenerator generator = new EntityModelSchemaGenerator(storeEntityContainer, conceptualSchemaNamespaceName, entityContainerName, pluralizationService);
generator.GenerateMetadata();
generator.WriteModelSchema(@"ConceptualSchema.csdl");
generator.WriteStorageMapping(@"MappingSchema.msl");
}
После выполнения этого кода в папке bin/Debug проекта появятся три файла: StoreSchema.ssdl, ConceptualSchema.csdl и MappingSchema.msl. Откроем ConceptualSchema и убедимся, что таблица Kunden была "сингуляризована" до Kunde:

<EntityType name="Kunde">
<Key>
<PropertyRef name="KundeId">
</PropertyRef>
<Property nullable="false" type="Int32" name="KundeId">
</Property>
</Key>
</EntityType>
Обратите внимание, что приведенный код добавляет в SSDL все доступные сущности базы данных. Однако при необходимости методу storeGenerator.GenerateStoreMetadata можно передать в качестве параметра список фильтров.

Получив три составляющие EDM схемы, скопируем их в нашу пустую модель (открыв ее при помощи XML Editor), сохраним ее и откроем в дизайнере. Теперь с этой моделью можно работать, как ни в чем не бывало.

Надеюсь, Вас не сильно утомил мой притянутый за уши пример кастомизации PluralizationService. К сожалению, более оригинального способа демонстрации этой возможности я придумать не смог.

На этом разговор о P/S в EF4 beta 1 закончен - пришло время поговорить о EF v. 1. Стандартной реализации подобного сервиса в EF v. 1 нет, однако есть замечательный плагин Huagati DBML/EDMX Tools (как можно догадаться по названию, он поддерживает не только EF, но и LINQ To SQL). Плагин не бесплатный, но с достаточно демократичными ограничениями на trial-версию. Так, например, согласно FAQ, можно снять со своей триал-лицензии ограничение в 50 сущностей на модель, просто написав письмо с соответствующей просьбой по адресу licensing@huagati.com. Таким же путем можно и продлить свою trial-лицензию. Кстати, этот продукт выпускается не только как плагин, но и как библиотека, позволяющая выполнять преобразования в runtime (правда, библиотека поставляется только с Professional-лицензией).

После установки плагина и выделения элемента в окне дизайнера (или щелчка по пустому месту) в меню Visual Studio появится новый элемент "DBML/EDMX Tools":
Сейчас нас интересует пункт "Standardize ADO.NET Entity Data Model class and member names". После щелчка по нему появится форма:
, которая позволяет редактировать имена сущностей, скалярных и навигационных свойств (но не во время создания или обновления модели, как в EF4, а постфактум). Как видим, функциональность плагина в плане P/S гораздо шире возможностей EF4 beta 1.

Генерация DDL по модели
В EF4 beta 1 появилась возможность генерации базы (точнее ее DDL) по модели.

Щелкаем правой кнопкой в свободном пространстве дизайнера -> выбираем Generate Database Script from Model...
В результате будет сгенерирован DDL, который условно можно разбить на две части: в первой удаляются текущие элементы базы данных, а во второй создаются новые.
Кликаем Finish -> дизайнер заботливо предупреждает нас о том, что Storage Scheme и Mappings будут перезаписаны (они будут сформированы на базе текущей концептуальной модели). Так, например, если Вы удалили из сущности Address свойство HomeNumber, в сгенерированном DDL его не будет, а, следовательно, его не будет ни в Storage Scheme, ни в Mapping.

В созданной дизайнером вкладке Visual Studio будет содержаться тот самый DDL:
Кликаем F5 -> выбираем базу -> OK -> база данных обновилась.

О кастомизации процесса генерации DDL подробно написано в блоге EF Design; а с тем, как применять описанные там возможности, можно ознакомиться в MSDN.

Плагин "Huagati DBML/EDMX Tools" тоже умеет генерировать DDL (опция "Generate Database (New Database)"). Правда, плагин генерирует лишь DDL для создание сущностей, поэтому при накате DDL на непустую базу, ее сущности придется предварительно удалить вручную.

В целом, целесообразность использования генерации DDL по EDM представляется мне весьма сомнительной. Ведь в EDM не хранится информация о тригерах, индексах и т. д., а значит при каждом выполнении сгенерированного DDL (который, как я уже отмечал, сначала удаляет все текущие элементы базы данных, а потом создает их заново) эти элементы будут потеряны. Вот если бы EF генерировал DDL, который вносил бы изменения в базу при помощи Alter, а не при помощи Drop+Create, думаю, толку от этой возможности было бы гораздо больше.

Удаление сущностей в дизайнере
Раз уж мы сегодня так много говорим о дизайнере (хотя и P/S, и генерация DDL - возможности обновленного edmgen; дизайнер - лишь обертка), грех не упомянуть изменения, коснувшиеся процесса удаления сущности из дизайнера (вот эта возможность - всецело заслуга дизайнера). В EF v. 1 при удалении сущности при помощи дизайнера она удалялась из CSDL и MSL, но оставалась в SSDL. Если бы Вы хотели удалить сущность, но при этом в дальнейшем использовать маппинг на таблицу, лежащую в ее "основе", поведение EF v. 1 - именно то, что Вам нужно. Однако при необходимости удалить сущность полностью приходилось открывать модель при помощи Xml Editor и вручную удалять все касающиеся данной сущности элементы из SSDL.

В EF4 разрешили данный дуализм и сделали это, на мой взгляд, очень красиво. При попытке удалить сущность Вы увидите следующее диалоговое окно:
При выборе No сущность будет удалена, как в EF v. 1, а при выборе Yes произойдет полное удаление. Очень удобно.

При использовании EF v. 1 нас опять выручит плагин "Huagati DBML/EDMX Tools". На этот раз выберем опцию "Cleanup SSDL/MSL/CSDL":
В левой колонке отображаются сущности, которые присутствуют в SSDL, но отсутствуют в CSDL и MSL. Можно либо удалить выбранные сущности полностью, нажав Remove, либо восстановить их описание в CSDL и MSL, нажав Create CSDL. В правой колонке, наоборот, отображаются сущности, которые присутствуют в CSDL, но отсутствуют в SSDL и MSL. В первую очередь, это касается сущностей, которые были созданы в дизайнере и еще не были замаплены. Опять же, плагин предлагает две опции: либо удалить описание сущности из концептуальной модели (Remove), либо создать для нее MSL и SSDL (маппинг будет произведен на таблицу базы данных с именем, совпадающим с именем сущности в концептуальной модели).

Таким образом, и в этот раз возможности плагина превосходят возможности EF4.

Заключение
Сегодня мы рассмотрели достаточно поверхностные изменения в EF: и P/S, и генерация DDL не являются критичным для ORM функционалом (приверженцы NHibernate долгое время вообще без дизайнера обходились). В следующий раз мы поговорим на более серьезную тему, на тему поддержки POCO. До скорых встреч!

Прикрепленный файл

четверг, 28 мая 2009 г.

Влюбляемся в Entity Framework: Шаг пятый: Выполнение запросов и маппинг

Выполнение запросов
В прошлый раз мы рассмотрели различные способы создания запросов, однако не акцентировали внимание на том, когда и как они выполняются. Пришло время восполнить это упущение.

За выполнение запросов в EF отвечает метод Execute уже знакомого нам класса ObjectQuery. В качестве аргумента метод Execute принимает параметр перечисления MergeOption. MergeOption определяет поведение кэша:
  • AppendOnly: Добавлять в кэш только новые записи. Существующие (в кэше) сущности не обновлять;
  • OverwriteChanges: Заменять текущие (current) значения существующих сущностей полученными значениями;
  • PreserveChanges: Заменять исходные (original) значения существующих сущностей полученными значениями. При этом текущие значения не изменяются, следовательно все изменения, внесенные до сих пор, остаются в силе;
  • NoTracking: Полученные значения не записываются в кэш
Трактовка значений MergeOption, данная выше, - перевод соответствующего раздела MSDN. Не считаю это описание интуитивно понятным, поэтому предлагаю разобраться с этим вопросом детальнее.

Рассмотрим небольшое тестовое приложение, которое демонстрирует, как значение MergeOption влияет на результат запроса:

using (FirstModel ctx = new FirstModel())
{
Person person = ctx.Persons.Where(p => p.PersonId == 1).First();

//После выполнения предыдущей строчки измените значение какого-нибудь свойства данной записи в базе вручную

ObjectQuery<person> personQuery = (ObjectQuery<person>)ctx.Persons.Where(p => p.PersonId == 1);

//Раскоментируйте поочередно нижележащие строчки и обратите внимание на возвращаемый результат
//Person person1 = personQuery.Execute(MergeOption.AppendOnly).First();
//person1.Person person2 = personQuery.Execute(MergeOption.OverwriteChanges).First();
//Person person3 = personQuery.Execute(MergeOption.PreserveChanges).First();
//Person person4 = personQuery.Execute(MergeOption.NoTracking).First();
}
Итак, после выполнения первой строчки у нас есть запись в кэше. Затем мы вручную меняем значение в базе и выполняем последующие запросы в четыре захода (для чистоты эксперимента).
  1. Полученное из базы значение игнорируется - возвращается значение из кэша;
  2. Текущие значения кэша подменяются значениями, полученными из базы - person2 содержит актуальные значения.
  3. Самый интересный случай. Как было описано выше, PreserveChanges лишь подменяет исходные значения, и не оказывает никакого влияния на текущие значения. Однако после выполнения теста в person3 будет содержаться актуальная информация из базы. Это связано с тем, что так как мы не производили никаких манипуляций с person, его текущие свойства не заданы. После выполнения запроса с PreserveChanges текущие значения останутся не заданными, поэтому в результате запроса будут возвращены исходные значения (которые к этому моменту уже были подменены на актуальные данные из базы). Если дополнить считывание person модификацией его свойств (например, если Вы планирует модифицировать в базе фамилию, достаточно дописать person.Surname = "AnySurname") - в person3 будут помещены текущие значения из кэша (в данном случае Surname будет равен AnySurname), а PreserveChanges, как ему и полагается, подменит лишь исходные значения.
  4. В случае NoTracking данные в кэш не заносятся и из кэша не считываются: person4 всегда будет содержать актуальные данные из базы.
Помимо явного вызова метода Execute, в EF есть ряд методов, делающих соответствующий вызов неявно (при этом в качестве MergeOption используется значение AppendOnly). Так, выполнение запроса происходит при вызове метода ObjectQuery.GetEnumenator, который (опять же неявно) происходит при использовании конструкции foreach, а также при вызове некоторых методов-расширений (например, ToList, ToArray и др.). Выполнение запроса произойдет и при вызове ряда других (не связанных с GetEnumenator) методов-расширений, таких как First/FirstOrDefault, Last/LastOfDefault и др.

Маппинг
Вот мы и добрались до самой "вкусной" возможности любой ORM-системы - до маппинга. К счастью, на эту тему уже написано достаточно статей, и повторяться я не буду. Я в очередной раз сошлюсь на статьи Сергея Розовика (раз, два), а опишу лишь маппинг Table splitting, которого в указанных статьях нет.

Table splitting
Представьте, что у Вас есть таблица, которая помимо относительно легких атрибутов содержит и тяжелые, вроде varbinary(max) или filestream (на примере SQL Server), причем в большинстве случаев требуются лишь легкие атрибуты:
  • Если мы храним в varchar(max) фотографию товара в высоком разрешении, то она понадобится лишь в тех редких случаях, когда пользователь интернет-магазина кликнет "посмотреть в полном размере";
  • Если мы храним в filestream содержимое файла библиотеки, то значение этого атрибута понадобится лишь при скачивании файла; во всех остальных случаях необходимы лишь описывающие файл атрибуты: имя, размер, дата загрузки и т. д.;
  • и т. д.
К нам на помощь приходит маппинг Table Splitting, который позволяет разделить работу с одной таблицей на несколько сущностей.

Добавим в таблицу Persons нашей базы данных (бэкап которой можно скачать в прикрепленном ко второй части файле) поле Photo типа varbinary(max). Для того чтобы не скачивать с сервера полуторамегабайтный jpeg каждый раз, когда нам нужна информация о пользователе, реализуем Table Splitting:
  1. В дизайнере скопируем и вставим сущность Person
  2. Переименуем полученную сущность в PersonPhoto (а ее Entity Set - в PersonPhotos) и в Mapping Details зададим Maps To Persons.
  3. Удалим из сущности Person свойство Photo, а из сущности PersonPhoto - свойства Name и Surname
  4. Добавим связь 1:1 между Person и PersonPhoto (правой кнопкой в окне дизайнера -> Add -> Association); затем выделим связь -> Mapping Details -> выберем Maps to Persons)
Сверим модель:
, Mapping Details сущности PersonPhoto:
и маппинг созданной связи:
Если сейчас попробовать сбилдить проект, в результате получим ошибку "Each of the following columns in table Persons is mapped to multiple conceptual side properties: Persons.PersonId is mapped to".

Для того чтобы избавиться от ошибки, необходимо создать ReferentialConstraint, описывающий связь между PersonId сущностей Person и PersonPhoto. К сожалению, дизайнер в EF v. 1 этой возможности не поддерживает, поэтому закроем дизайнер -> щелкнем правой кнопкой по FirstModel.edmx в Solution Explorer -> Open With... -> XML Editor... -> в разделе CSDL найдем определение ассоциации PersonPersonPhoto:

<Association Name="PersonPersonPhoto">
<End Type="FirstModelModel.Person" Role="Person" Multiplicity="1" />
<End Type="FirstModelModel.PersonPhoto" Role="PersonPhoto" Multiplicity="1" />
</Association>
и добавим в него ReferentialConstraint:

<Association Name="PersonPersonPhoto">
<End Type="FirstModelModel.Person" Role="Person" Multiplicity="1" />
<End Type="FirstModelModel.PersonPhoto" Role="PersonPhoto" Multiplicity="1" />
<ReferentialConstraint>
<Principal Role="Person">
<PropertyRef Name="PersonId" />
</Principal>
<Dependent Role="PersonPhoto">
<PropertyRef Name="PersonId" />
</Dependent>
</ReferentialConstraint>
</Association>
Вот теперь нас поджидает successful build.

Воспользуемся созданным маппингом и добавим запись в таблицу:

using (FirstModel ctx = new FirstModel())
{
Person person = new Person();
person.Name = "X";
person.Surname = "Y";

PersonPhoto personPhoto = new PersonPhoto();
personPhoto.Photo = new byte[] { 1, 2, 3, 4, 5 };

person.PersonPhoto = personPhoto;

ctx.AddToPersons(person);

ctx.SaveChanges();
}
При этом будет сгенерирован один INSERT-запрос, в котором задаются все поля (в том числе и Photo), а это значит, что маппинг работает. Правда, есть один нюанс. Так, если при создании сущности необходимо оставить свойству Photo значение NULL, нельзя просто не задавать его, как это обычно делается со скалярынми свойствами. Загвоздка в том, что таблицы Person и PersonPhoto связаны 1:1, а связать их 1:0..1 невозможно, ибо связывается первичный ключ одной и той же таблицы. Поэтому необходимо явно присвоить свойству Photo экземпляр класса PersonPhoto:

using (FirstModel ctx = new FirstModel())
{
Person person = new Person();
person.Name = "X";
person.Surname = "Y";

person.PersonPhoto = new PersonPhoto();

ctx.AddToPersons(person);

ctx.SaveChanges();
}
Считывать значения PersonPhoto можно следующими способами:
  • можно считать непосредственно PersonPhoto (для него создан свой EntitySet), отобрав записи по PersonId;
  • можно считать Person, а затем при помощи defered-loading подгрузить навигационное свойство PersonPhoto;
  • а можно, считывая Person, загрузить навигационное свойство PersonPhoto при помощи eager-loading:

using (FirstModel ctx = new FirstModel())
{
List<Person> persons = ctx.Persons.Include("PersonPhoto").ToList();

...
}
Сгенерированный в этом случае запрос радует глаз: вся загрузка сводится к одному простенькому SELECT'у (точно такому, какой бы Вы написали вручную).

Заключение
В мои планы входило описание еще одного редко встречающегося типа маппинга - Table per concrete type (TPC). Однако столкнувшись с рядом ограничений, а также с несовместимостью Table splitting и TPC, отказался от этой идеи. Надеюсь, в следующей версии реализация TPC будет улучшена.

На этом посте данный цикл статей будет временно заморожен. Хотя некоторое время спустя я, наверное, к нему вернусь. В ближайшее же время Вас ждет новый цикл статей, который (уж не знаю, к счастью или к сожалению) опять-таки будет посвящен Entity Framework. До скорых встреч!

воскресенье, 24 мая 2009 г.

Сравнительный анализ фреймворков для работы с онтологиями под .NET и Java

Ни для кого не секрет, что львиная доля проектов, связанных с Semantic Web, разрабатывается на Java. Фреймворки для работы с семантическими онтологиями не являются исключением: все основные проекты (Jena, OWL API, Sesame и т. д.) написаны именно на Java. Единственным серьезным представителем, использующим .NET, является фирма Intellidimension с продуктами RDF Gateway и Semantics.SDK.

В этой статье опишу свой опыт работы с вышеуказанными фреймворками и поделюсь результатами тестирования.

Вступление
Данная статья не является всеобъемлющим обзором вышеуказанных фреймворков. Статья направлена на анализ производительности базовых возможностей фреймворков: загрузки онтологии, логического вывода и выполнения SPARQL-запросов.

Перед погружением в технические детали скажу пару слов о фреймворке от Intellidimension (как наименее известном продукте для Java-ориентированного сообщества). В отличие от остальных рассматриваемых в этой статье фреймворков, которые являются OpenSource-проектами, RDF Gateway и Semantics.SDK распространяются с закрытыми исходными кодами и стоят достаточно приличных денег. Так, один лишь RDF Gateway 3.0 Enterprise стоит 10000$ (хотя версия 2.0 стоила «всего» 2000$). К слову, использованные при тестировании ризонеры - Pellet и Owlim - тоже не бесплатны: Pellet распространяется по dual license, а Owlim предлагает бесплатно лишь версию, работающую в памяти; версия, работающая с хранилищем, стоит 700 евро за каждое используемое процессорное ядро.

Тестирование
Передо мной стояла задача подобрать фреймворк для реализации проекта под .NET, поэтому Java-проекты в чистом виде меня не интересовали (изначально я даже не планировал их тестирование). Необходимо было выбрать средство interop’а между Java и .NET. Мой выбор пал на ikvm.net, который позволяет конвертировать jar’ы в .NET dll. Получив .NET-версию Jena, OWL API и Sesame, я принялся за их тестирование. Однако тестирование было бы неполным, если бы не содержало результатов тестирования Java-фреймворков в их родной среде. Таким образом, в тестировании участвуют: Intellidimension Semantics.SDK 1.0, OWL API 2.2.0+Pellet 2.0rc5 (как под Java, так и под .NET), Jena 2.5.7+Pellet2.0rc5 (как под Java, так и под .NET) и отчасти Sesame 2.24+SwiftOwlim 3.0b9.

Из первоначального тестирования пришлось исключить Sesame ввиду отличающейся от Semantic SDK и Pellet политики логического вывода в Owlim (Owlim - основной ризонер, используемый в связке с Sesame). Так, Pellet и Semantic SDK нацелены на вывод во время запроса (query-time reasoning), хотя и включают средства заблаговременного вывода; Owlim же нацелен на полный логический вывод (full materialization). Об этом мы еще поговорим в разделе «Общая информация о ризонерах».

В качестве тестовой была выбрана онтология Thesaurus 09.02d института NCI. Правда, в чистом виде она содержит ряд противоречивостей (inconsistency). После общения со службой поддержки противоречивости были выявлены. Я использовал модифицированную версию 09.02d (которую Вы можете скачать с моего dropbox’а), хотя сейчас уже доступна версия 09.04d, в которой данных противоречивостей нет.

Для тестирования была смоделирована следующая ситуация:
1. Сначала онтология из файла загружалась в модель;
2. Затем к этой модели последовательно выполнялось 3 SparQL-запроса (тексты запросов можно скачать здесь).

Сначала рассмотрим результаты тестирования первого этапа:
Несмотря на то, что .NET/ikvm-фреймворки медленнее своих Java-собратьев в 2-3 раза, они оказались быстрее Intellidimension.

В плане работы с памятью .NET оставил более приятное впечатление. Сборщик мусора в Java требует указания максимального объема оперативной памяти, который может быть выделен под кучу (параметр Xmx); .NET же использует более логичную, на мой взгляд, политику: он потребляет столько памяти, сколько ему нужно (если только не задано ограничение). Ограничение через Xmx - очень старый "баг", который, к сожалению, до сих пор не пофикшен. В качестве решения предлагается просто задавать Xmx с запасом (если, конечно, объем оперативной памяти позволяет), однако в этом случае (тестирование велось с Xmx:12g) сборщик мусора особенно себя не утруждает (что мы и видим по результатам тестирования). Можно пойти другим путем - подбирать минимальное значение Xmx для конкретных входных данных (правда, рискуя напороться на OutOfMemoryException). Таким образом можно приблизить объем потребляемой JVM памяти к аналогичным показателям CLR (пусть и с некоторыми дополнительными потерями производительности на более частые сборки мусора).

Касательно ограничения на максимальный объем используемой памяти произошел довольно курьезный случай. После успешной загрузки онтологии Thesaurus при помощи конвертированного в .NET OWL API, я решил открыть эту онтологию в Protége (который базируется на той же версии OWL API) для ознакомления с ее структурой. Однако вместо дерева концептов и экземпляров я получил OutOfMemoryException (при том, что свободной памяти было предостаточно). И хотя увеличение значения атрибута Xmx разрешило проблему, такая несамостоятельность сборщика мусора в Java не радует. Курьезность ситуации заключается в том, что несмотря на то, что Java-приложение не запускается в родной среде (без танцев с JVM), после конвертации в .NET при помощи ikvm оно работает.

Теперь перейдем ко второму пункту тестирования - выполнению SPARQL-запросов. К сожалению, на этом этапе придется оставить за бортом лидера первого теста - OWL API. Дело в том, что интерфейс OWL API, который реализуют ризонеры, не содержит методов для выполнения SPARQL-запросов. Связано это с тем, что SPARQL создавался как язык запросов к RDF-графам (и с OWL он не очень дружит), а OWL API, как нетрудно догадаться по названию, ориентирован именно на OWL. Сейчас идет работа над стандартом SPARQL-DL и, возможно, его поддержка будет реализована в одной из следующих версий OWL API. На данный момент остается лишь использовать class expressions, которые позволяют писать запросы с использованием манчестерского синтаксиса. Class expressions - это, конечно, не SPARQL… но для большинства задач их достаточно.

Итак, результаты тестирования:
Сначала прокомментирую прочерки в графе Intellidimension. С момента запуска тестового приложения прошло 6 часов, процесс весил порядка 6Гб, а результата все не было. Ждать дольше у меня не хватило терпения. Вынужден засчитать Semantics.SDK техническое поражение. Справедливости ради стоит отметить, что с более мелкими онтологиями Semantics.SDK справляется: и вывод делает, и запросы обрабатывает… правда, сравнивая результаты с Jena+Pellet, могу с уверенностью заявить, что Semantics.SDK не всегда выдает полный результат.

Pellet же справился с выводом за вполне разумное время (запросы были не из легких) и, как и на первом этапе тестирования, .NET/ikvm-фреймворк выглядит предпочтительнее Intellidimension.

На этом этап тестирования закончен. Подводя итоги, можно сказать, что победителем стала система Jena+Pellet, а приз авторских симпатий достается OWL API+Pellet.

Общая информация о ризонерах
В целом, существует два подхода к реализации логического вывода: на базе правил (с использованием алгоритмов forward-chaining и/или backward-chaining) и на базе семантического табло (semantic tableau). На базе правил реализованы Semantics.SDK и Owlim, а на базе семантического табло - Pellet.

Насколько я могу судить, rule-based ризонеры выгодно использовать для языков с низкой выразительностью, а ризонеры на базе семантического табло - для языков с высокой выразительностью. Если Pellet (OWL-DL) и Owlim (OWL-Tiny) подтверждают это наблюдение (находясь по разные стороны баррикады), то Semantics.SDK (OWL-FULL) является исключением (и судя по тестам производительности, ничего хорошего в этом исключении нет).

Рассмотрим диаграмму c официального сайта Owlim:
Как видим, Owlim поддерживает лишь OWL Tiny. Именно эта особенность позволяет ему добиться очень высокой производительности.

Owlim - единственный (из рассмотренных) ризонер, который поддерживает многопоточный вывод. Тем самым Owlim реализует преимущество rule-based систем перед семантическим табло - возможность распараллеливания (алгоритмов распараллеливания процесса построения семантического табло пока нет). Разработчики Semantics.SDK утверждают, что их вывод тоже распараллеливается… но в trial-версии это не реализовано (на мой взгляд, не самое удачное ограничение для trial-версии). Просьба выделить мне краткосрочную лицензию для тестирования осталась без ответа, поэтому приходится верить разработчикам на слово.

В идеальном мире определенно должен был бы существовать ризонер, который бы при анализе онтологии определял уровень выразительности и применял для ее вывода соответствующие алгоритмы. На данный момент такого ризонера нет, да и вряд ли он когда-либо появится.

Заключение
В этой статье акцент сделан на производительности, однако при выборе семантического фреймворка для того или иного проекта акцент нужно, в первую очередь, делать на функционале (поддержка (нереляционного) хранилища, поддержка OWL2 и т. д.). Обсуждение этих вопросов выходит за рамки данной статьи (да и не настолько хорошо я знаком с каждым из фреймворков, чтобы делать такой анализ). В отношении функциональности я хотел было похвалить продукты от Intellidimension: связка RDFGateway и Semantics.SDK представляет собой очень мощный фреймворк, аналогов которому в мире Java нет… но выкрутасы их ризонера охладили мои чувства к этому фреймворку.

P. S. Огромное спасибо за ликбез, оказанный в ходе написания статьи, Клинову Павлу.
PP. S. Статья также опубликована на shcherbak.net и на Хабре. Контент там тот же, а комменты (равно как и сообщество) совершенно разные - советую пролистать.