Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Data acquisition, часть 3

4 комментария

В предыдущих моих постах (часть 1, часть 2) я описал то, как получить данные из интернета как HTML, как настроить простой сервис для регулярной загрузки данных, как скорректировать HTML и загрузить его в CLR-объект. В этом посте мы обсудим то, как хранить и обновлять данные в базе. Также я приведу полное описание процесса скрейпинга.

Избежание повторов путем автогенерации UPSERT (MERGE DML)

Если у вас крутится сервис который записывает хоть что-то в базу, важно избежать повторов, т.е. дубликации записей. Решение проблемы – создание UPSERT-процедуры. Upsert – это update или insert в зависимости от того, есть уже запись или нет. Если записи нет, ее можно добавить. Если она уже есть, ее можно обновить.

В SQL Server 2008 вместо триггеров и прочих извратов можно воспользоваться инструкцией MERGE, которая создана специально для реализации UPSERT-поведения. Одна проблема – инструкция эта сама по себе выглядит ужасно, поэтому лучше всего ее автогенерировать из сушествующих сущностей.

Мой подход к генерации MERGE DML примерно такой: поскольку ORM не хранит информацию относительно того, какие элементы должны совпадать чтобы это был действительно UPDATEd а не INSERTed, мне проще всего контролировать этот файл вручную. С другой стороны, у меня обязательно присутствует та или иная модель, и хочется использовать именно ее для генерации начальных данных.

Посмотрим как это делается с использованием EF4.0. В EF у нас есть файл с расшинением EDMX, и если копнуть его вглубь по XPath-у Edmx/Runtime/ConceptualModels/Schema, мы получим описание всех сущностей. Для того чтобы их замэпить на что-то-там, нужно сначала найти схему System.Data.Resources.CSDLSchema_2.xsd – находится она там же где установлена Студия, в папке \xml\Schemas.

Для сущностей не получится трансформировать EDMX сразу в SQL по ряду причин – во-первых, нам не замэпить схему EDMX т.к. она составная и не парсится, ну и если бы даже мы ее замэпили, пришлось бы редактировать созданный SQL для удаления из него тех сравнений, которые являются “образующими”. Сейчас объясню что к чему.

Итак, возьмем типичный случай – сущность Person { Name, Age } которую нужно обновлять (возраст меняется) или добавлять новую (если имя новое).

Первое что мы делаем – выдираем секцию <Schmema> из концептуальной схемы. Получаем примерно следующее:

<Schema>
  <EntityContainer Name="ModelContainer" annotation:LazyLoadingEnabled="true">
    <EntitySet Name="People" EntityType="Model.Person"/>
  </EntityContainer>
  <EntityType Name="Person">
    <Key>
      <PropertyRef Name="Id"/>
    </Key>
    <Property Type="Int32" Name="Id" Nullable="false" annotation:StoreGeneratedPattern="Identity"/>
    <Property Type="String" Name="Name" Nullable="false"/>
    <Property Type="Int32" Name="Age" Nullable="false"/>
  </EntityType>
</Schema>

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

В результате трансформации мы получаем примерно такой документ:

<tables>
  <table name="Person">
    <field type="String" name="Name"/>
    <field type="Int32" name="Age"/>
  </table>
</tables>

Поле Id сюда не попало, т.к. в Upsert-операции мы не делаем сравнение по Id. (С другой стороны, следует помнить что в сгенерированной процедуре мы возвращаем SCOPE_IDENTITY(), поэтому неполучится дать Id тип вроде uniqueidentifier.) Затем, этот документ трансформируется другим XSLT (которому уже много лет :) и в результате получается именно то, что нужно, а именно:

/* Check that the stored procedure does not exist, and erase if it does. */
if object_id ('dbo.PersonUpsert', 'P') is not null
  drop procedure [dbo].[PersonUpsert];
go
/* Upserts an entry into the 'Person' table. */
create procedure [dbo].[PersonUpsert](
  @Id int output,
  @Name nvarchar(max),
  @Age int)
as
 begin
  merge People as tbl
   using (select
    @Name as Name,
    @Age as Age) as row
   on
    tbl.Name = row.Name
when not matched then
  insert(Name,Age)
  values(row.Name,row.Age)
when matched then
 update set
  @Id = tbl.Id,
  tbl.Name = row.Name,
  tbl.Age = row.Age
;
if @Id is null
  set @Id = SCOPE_IDENTITY()
return @Id
end

Теперь эта хранимая процедура мэпится на EF, Linq2Sql или какой-то другой ORM, и ее можно использовать. Вот пример в EF4:

var op = new ObjectParameter("Id", typeof(Int32));
using (var mc = new ModelContainer())
{
  // add me
  mc.PersonUpsert(op, "Dmitri", 25);
  mc.SaveChanges();
}

В примере выше мы также пожем проверить, был ли добавлен новый объект или обновлен старый, и в любом из случаев мы сможем получить Id объекта для последующего использования. Конечно, в типичном сценарии использования все эти процессы реализованы через Repository/UnitOfWork со всякими там TransactionScope и иже с ними.

Замечу что вполне возможно вместо “двойного прыжка” с XSLT сделать один Т4 файл которые все сам бы делал, но это настолько нудная задачка, что в принципе легче сделать так как я описал. Конечно, тот факт что придется выдирать <Schema> из EDMX это тоже неидеально, но пока сойдет. Кстати, хочу также заметить что по непонятным причинам (а может я плохо искал) не существует мэппера который мог бы мэпить XML на TXT и при этом производить XSLT-трансформацию. Я глянул на FlexText, но эта программа не позволила мне сделать вставки в строках, а также MapForce порождал с помощью нее только C#, а делать XSLT отказался.

Полное описание процесса

Настало время полностью описать процесс создания типичного скрейпера. Если коротко, то в типичной реализации мы производим следующие действия:

  • Находим те страницы которые нужно обработать и смотрим на них с помощью FireBug
  • Скачиваем страницы – если нужна сложная аутентификация или ввод со стороны польователя, используем WatiN, иначе используем WebRequest и т.п.
  • Находим на страницах те элементы что нам нужны и
    • Трансформируем элементы чтобы сделать их XML-совместимыми
    • Делаем сущность (entity) для хранения данных из этого куска XML
    • Делаем класс-коллекцию Collection<T> для этой сущности
    • Генерируем для класса-коллекции соответствующий XSD с помощью xsd -t:MyCollection MyAssembly.exe
    • Автогенерируем XSD с исходного HTML
    • Создаем мэппинг с одного XSD на другой
  • В коде, делаем мэппинг с обработанного HTML на XML
  • Считываем сущность или коллекцию сущностей из полученного XML
  • Создаем Upsert-процедуру (пример):

    • Выдираем элемент <Schema> из WSDL
    • Трансформируем элемент в упрощенную форму
    • Трансформируем результирующий XML в SQL для создания хранимой процедуры
    • Создаем хранимую процедуру в базе
    • Импортируем хранимую процедуру в наш ORM
  • После создания сущности, записываем ее в базу (обновляем или создаем новую)

Вот как-то так. Конечно, наверняка есть более простые пути. Опять же, как уже кто-то писал, вместо мэппингов можно использовать Linq напрямую, и в простых сценариях это вполне хорошо работает. Удачи!

Advertisements

Written by Dmitri

27 мая 2010 в 19:26

Опубликовано в .NET, Technology

комментария 4

Subscribe to comments with RSS.

  1. А зачем скаченый хтмл превращать в валидный хмл а затем еще и мапить в сущность. Не проще регулярными выражениями выдрать нужные данные с хтмл и положить это все в базу. Ну в крайнем случае создать экземпляр сущности по этим данным, если нужен доп. анализ перед складыванием в бд. Ведь у таких data-manning процессоров один из главных параметров скорость работы, а тут создается столько прослоек которые будут жрать время да и память.

    headachy

    28 мая 2010 at 12:50

    • Все зависит от масштаба решения и требованиям в плане поддерживаемости и документирования. Простой сценарий вполне реализуется с Linq, RegExp’ами и т.п., но более сложные задачи требуют систематичного подхода.

      Dmitri

      28 мая 2010 at 17:35

  2. Дмитрий, можно вопрос по WatiN задать по электронной почте?

    Виталий

    28 мая 2010 at 17:16


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: