Введение в ObjectSpaces

Введение в ObjectSpaces

Тимофей Казаков (TK) 

Сохраняемые объекты

В .NET Framework управление данными осуществляется на уровне объектов. Каждый объект характеризуется своим состоянием (свойства), поведением (методы), и является экземпляром какого-либо конкретного класса. В рамках приложения классы могут различаться по целевому назначению – это могут быть элементы управления, отображающие интерфейс пользователя, или сервисные классы, отвечающие за связи с базами данных и работу с сетевыми функциями, это могут быть классы “сообщений”, обеспечивающие обмен информацией между частями приложения. Все эти сущности объединяет одна общая черта – время их жизни обычно не превышает времени жизни всего приложения. Но, кроме вышеперечисленных категорий классов, можно выделить целый ряд сущностей, время жизни которых превышает срок жизни приложения. Например, в бизнес-задачах роль подобных сущностей могут играть объекты “Клиент”, “Заказчик”, “Продукт”. Таким объектам необходимо предоставить возможность сохранения своего состояния во внешнее хранилище. 

В .NET Framework существуют готовые средства для работы с сохраняемыми объектами, – есть возможность сохранять состояние объектов в двоичном виде с использованием BinaryFormatter или XML-формате с использованием XmlSerializer. Все эти средства предоставляют возможности сохранения “графов” объектов, однако сохраняемая информация не оптимизирована для выполнения запросов к хранимым данным – так, поиск необходимой информации в XML-файле, содержащем несколько тысяч записей, может оказаться неприемлемо медленным. В большинстве подобных случаев в качестве хранилища информации подойдет реляционная СУБД – данные сохраняются в таблицах, для дополнительного контроля целостности между ними устанавливаются отношения, поиск информации осуществляется с использованием языка запросов SQL. Аналогичную функциональность предоставляют специальные библиотеки Object/Relational Mapping (O/R Mapping). Такая библиотека перекладывает на себя всю “черную” работу по сохранению/загрузке информации из объектной модели приложения в реляционную модель базы данных. В .NET Framework 1.2 для этих целей есть специальный набор классов из пространства имен System.ObjectSpaces.*.

ObjectSpaces

Если раньше, используя ADO.NET, нужно было самостоятельно писать SQL-запросы, то теперь это требование становится необязательным – ObjectSpaces берут на себя всю заботу об отображении классов приложения на различные источники данных. При этом мы можем создавать новые объекты, сохранять их, выполнять различные запросы - все необходимые действия по взаимодействию с источником данных будут выполняться внутри ObjectSpaces (при этом данные могут находиться как в традиционной БД, так и быть представленными в XML форме)

ПРЕДУПРЕЖДЕНИЕ

Текущая версия ObjectSpaces поддерживает в качестве источника данных только SQL Server 7.0 и выше.

Для объектов приложения ObjectSpaces предоставляет следующие возможности:

Прозрачное отображение экземпляров .NET объектов на источник данных. 

Поддержку иерархий классов для сохраняемых объектов. 

Сохранение взаимосвязей между объектами (один к одному, один ко многим, многие ко многим). 

Отложенную загрузку связанных объектов. Построение запросов с использованием OPath. 

Архитектура ObjectSpaces

Какая функциональность требуется от O/R Mapping-библиотеки? Кроме очевидных задач – загрузки/сохранения состояния объекта и выполнения операций поиска, есть и менее очевидные задачи – отслеживание состояния и идентификация объекта. Для чего это нужно? 

Отслеживание состояния требуется для принятия решения о необходимости сохранения объекта. Совершенно очевидно, что если ни одно из полей объекта не изменялось, то повторно сохранять ту же информацию совершенно не обязательно. Информация об оригинальных значениях полей может понадобиться и для достижения “оптимистической параллельности” (optimistic concurrency) в ситуациях, когда в БД нет колонки с версией записи. Также можно оптимизировать сохранение полей объекта для ситуаций, когда один объект отображается на несколько таблиц в базе данных, просто не обновляя не изменившиеся данные. 

ПРИМЕЧАНИЕ

Optimistic concurrency (оптимистический параллелизм) – это возможность двум независимым клиентам редактировать одну и ту же информацию без дополнительной блокировки каких-либо ресурсов. Все проверки относительно правомочности сделанных изменений осуществляются только в момент сохранения записи. Это можно реализовать, например, добавлением в таблицу специального поля для идентификации версии записи (например, timestamp).

В каких случаях нужно уметь идентифицировать объект? В случае с O/R Mapping-библиотекой мы работаем не с “сырыми” данными, а с реальными объектами. Это значит, что одному значению первичного ключа в базе данных должен соответствовать один объект в приложении. В самом деле, разумно рассчитывать, что все возможные способы получения одного и того же объекта из базы данных каждый раз должны возвращать одну и ту же ссылку. Это означает, что O/R Mapping-библиотека должна отслеживать все загружаемые объекты, и в случае повторной попытки восстановить объект с тем же значением первичного ключа возвращать ссылку на уже загруженный. 

Какие есть пути для реализации подобной функциональности? Реализация функциональности идентификации объектов пересекается с реализацией отслеживания состояния объекта и не представляет особой сложности. Поэтому сосредоточимся на том, какими способами можно обеспечить отслеживание состояния.

Есть два варианта отслеживания состояния объекта. В первом варианте инициатором сохранения состояния выступает сам объект. По второму варианту объект играет пассивную роль, а вся необходимая функциональность реализуется в библиотеке O/R Mapper. Разберем каждую из двух реализаций более подробно.

Инициатором сохранения выступает объект. В данной ситуации O/R Mapper предоставляет механизм хранения оригинальных и текущих значений, а “сохраняемый” объект выступает лишь интерфейсом для работы c этим хранилищем. Здесь можно выделить два возможных направления:

“Сохраняемый” класс описывается на некотором метаязыке. Что это за метаязык, и какие средства работы с ним используются, в общем случае не столь важно. Отличительной особенностью данной реализации является то, что весь необходимый код отслеживания состояния генерируется на этапе разработки (компиляции). В качестве примера подобного подхода можно взять реализацию Borland Enterprise Core Objects (ECO).

Все свойства, для которых необходимо предоставить возможности “сохранения” объявляются как абстрактные, сам такой класс также становится абстрактным (MustInherit в VisualBasic). На O/R Mapping-библиотеку в такой ситуации ложится ответственность за создание наследника “сохраняемого” (например, через Reflection.Emit) класса с неизбежной реализацией всех сохраняемых свойств, и предоставление фабрики класса для его создания. Найти реализацию подобной функциональности можно в ранней preview-версии ObjectSpaces и в EntityBroker (http://www.thona-consulting.com).

У каждого из этих направлений есть свои достоинства и недостатки. Плюсом первого направления является более быстрый запуск, т.к. весь необходимый код сгенерирован еще на этапе компиляции. Между тем, этот плюс может стать и минусом – если по какой-то причине реализация “отслеживания” состояния изменится, придется повторить операцию генерации кода сохраняемого класса и перекомпилировать его. В случае использования второго направления будет достаточно заменить реализацию O/R Mapper.

Объект играет пассивную роль, вся реализация – в O/R Mapper. В этой ситуации к “сохраняемому” классу не предъявляется никаких дополнительных требований. Основной плюс данного подхода состоит в том, что для добавления “сохраняемости” к существующему классу его не надо никак модифицировать (максимальная прозрачность в использовании O/R Mapper), а минус состоит в том, что для сохранения закрытых полей класса приходится использовать механизмы рефлексии, что может отрицательно сказаться на производительности. 

В ObjectSpaces выбран второй вариант. В основе реализации ObjectSpaces (Рисунок 1) лежат три основных класса: ObjectEngine, ObjectContext и ObjectSpace. 

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

ObjectContext – это реализация механизмов идентификации и отслеживания состояния “сохраняемых” классов. 

ObjectSpace. Класс ObjectSpace на более высоком уровне объединяет функциональность, заложенную в реализациях ObjectEngine и ObjectContext. Для отображения реляционной модели данных в объектную ObjectSpace использует набор XML-схем, описываемых классом MappingSchema. 



Рисунок 1 Архитектура ObjectSpaces. 

Схемы данных

Для большинства приложений описать однозначное (“один к одному”) отображение объектной модели данных на реляционную модель нельзя, иногда нужно специально описывать то, как объекты должны отображаться на источник данных. В ObjectSpaces эту задачу выполняет класс MappingSchema (пространство имен System.Data.Mapping). Данный класс предназначен для описания: 

RSD (Relational Schema Definition) – схемы, которая описывает таблицы, поля и отношения между ними; 

OSD (Object Schema Definition) – схемы, описывающей объекты; 

MSD (Mapping Schema Definition) – схемы отображения. 

ObjectSpaces дает возможность самостоятельно формировать состояние класса MappingSchema или загружать его состояние из XML-файла. Рассмотрим использование MappingSchema на основе базы данных Northwind из состава SQL Server. На первом этапе нужно описать структуру этой базы данных в RSD-схеме:

<rsd:Database Name="Northwind" Owner="sa"

 xmlns:rsd="http://schemas.microsoft.com/data/2002/09/28/rsd">

 <rsd:Schema Name="dbo">

  <rsd:Tables>

   <rsd:Table Name="Orders">

    <rsd:Columns>

     <rsd:Column AutoIncrement="true" Name="OrderID" SqlType="int"

           IncrementStep="1" IncrementSeed="1"/>

     <rsd:Column AllowDbNull="true" Name="CustomerID" SqlType="nchar"

           Precision="5"/>

     <rsd:Column AllowDbNull="true" Name="EmployeeID" SqlType="int"/>

     <rsd:Column AllowDbNull="true" Name="OrderDate" SqlType="datetime"/>

     <rsd:Column AllowDbNull="true" Name="RequiredDate" SqlType="datetime"/>

     <rsd:Column AllowDbNull="true" Name="ShippedDate" SqlType="datetime"/>

     <rsd:Column AllowDbNull="true" Name="Freight" SqlType="money">

      <rsd:Default>0</rsd:Default>

     </rsd:Column>

    </rsd:Columns>

    <rsd:Constraints>

     <rsd:PrimaryKey Name="PK_Orders">

      <rsd:ColumnRef Name="OrderID"/>

     </rsd:PrimaryKey>

     <rsd:ForeignKey ForeignTable="Customers" Name="FK_Orders_Customers">

      <rsd:ColumnMatch ForeignName="CustomerID" Name="CustomerID"/>

     </rsd:ForeignKey>

    </rsd:Constraints>

   </rsd:Table>

   <rsd:Table Name="Customers">

    <rsd:Columns>

     <rsd:Column Name="CustomerID" SqlType="nchar" Precision="5"/>

     <rsd:Column Name="CompanyName" SqlType="nvarchar" Precision="40"/>

     <rsd:Column AllowDbNull="true" Name="ContactName" 

           SqlType="nvarchar" Precision="30"/>

     <rsd:Column AllowDbNull="true" Name="Phone" SqlType="nvarchar"

           Precision="24"/>

    </rsd:Columns>

    <rsd:Constraints>

     <rsd:PrimaryKey Name="PK_Customers">

      <rsd:ColumnRef Name="CustomerID"/>

     </rsd:PrimaryKey>

    </rsd:Constraints>

   </rsd:Table>

  </rsd:Tables>

 </rsd:Schema>

</rsd:Database>

Эта схема описывает две таблицы из базы данных Northwind (рисунок 2). Для таблиц Customers и Orders описываются исходные поля в БД, первичные ключи, а также связи между таблицами.



Рисунок 2. ER-диаграмма 

Кроме этого, понадобится описать OSD-схему, которая будет описывать объекты C#-кода. 

<osd:ExtendedObjectSchema Name="DataTypesOSD"

 xmlns:osd="http://schemas.microsoft.com/data/2002/09/20/persistenceschema">

 <osd:Classes>

  <osd:Class Name="Rsdn.Samples.Northwind.Customer">

   <osd:Member Name="CustomerID" Key="true" />

   <osd:Member Name="Company" />

   <osd:Member Name="Name" />

   <osd:Member Name="Phone" />

   <osd:Member Name="Orders" />

  </osd:Class>

  <osd:Class Name="Rsdn.Samples.Northwind.Order">

   <osd:Member Name="_orderID" KeyType="AutoIncrement" 

         Hidden="false" Key="true" Alias="OrderID" />

   <osd:Member Name="OrderDate" />

   <osd:Member Name="RequiredDate" />

   <osd:Member Name="ShippedDate" />

   <osd:Member Name="EmployeeID" />

   <osd:Member Name="Freight" />

   <osd:Member Name="Customer" />

  </osd:Class>

 </osd:Classes>

 <osd:ObjectRelationships>

  <osd:ObjectRelationship Name="Customers_Orders" 

     Type="OneToMany" ParentClass="Rsdn.Samples.Northwind.Customer"

     ParentMember="Orders" ChildClass="Rsdn.Samples.Northwind.Order" 

     ChildMember="Customer" />

 </osd:ObjectRelationships>

</osd:ExtendedObjectSchema>



Рисунок 3. Объектная модель. 

После объявления RSD- и OSD-схем (рисунок 3), нужно создать Mapping-схему, которая определит отображение одной схемы на другую:

<m:MappingSchema xmlns:m="http://schemas.microsoft.com/data/2002/09/28/mapping">

 <m:DataSources>

  <m:DataSource Name="NorthwindRSD" Type="SQL Server" Direction="Source">

   <m:Schema Location="RSD.XML" />

   <m:Variable Name="Customers" Select="Customers" />

   <m:Variable Name="Orders" Select="Orders" />

   <m:Relationship Name="Customers_Orders" 

           FromVariable="Customers" ToVariable="Orders">

    <m:FieldJoin From="CustomerID" To="CustomerID" />

   </m:Relationship>

  </m:DataSource>

  <m:DataSource Name="DataTypesOSD" Type="Object" Direction="Target">

   <m:Schema Location="OSD.XML" />

  </m:DataSource>

 </m:DataSources>

 <m:Mappings>

  <m:Map SourceVariable="Customers" 

      TargetSelect="Rsdn.Samples.Northwind.Customer">

   <m:FieldMap SourceField="CustomerID" TargetField="CustomerID" />

   <m:FieldMap SourceField="CompanyName" TargetField="Company" />

   <m:FieldMap SourceField="ContactName" TargetField="Name" />

   <m:FieldMap SourceField="Phone" TargetField="Phone" />

  </m:Map>

  <m:Map SourceVariable="Orders" 

      TargetSelect="Rsdn.Samples.Northwind.Order">

   <m:FieldMap SourceField="OrderID" TargetField="_orderID" />

   <m:FieldMap SourceField="OrderDate" TargetField="OrderDate" />

   <m:FieldMap SourceField="RequiredDate" TargetField="RequiredDate" />

   <m:FieldMap SourceField="ShippedDate" TargetField="ShippedDate" />

   <m:FieldMap SourceField="EmployeeID" 

         TargetField="EmployeeID" NullValue="-1" />

   <m:FieldMap SourceField="Freight" TargetField="Freight" />

  </m:Map>

  <m:RelationshipMap Source="Customers_Orders" Target="Customers_Orders" />

 </m:Mappings>

</m:MappingSchema>

OPath

Одна из основных задач при работе с информацией – это создание запросов для выборки необходимых данных. Так, в случае РСУБД можно использовать язык запросов SQL, для выборки информации из XML-источников у нас есть XPath. Но как SQL, так и XPath – это языки запросов, которые слишком сильно привязаны к модели хранения данных и, как результат, для O/R Mapper приходится применять специальный язык запросов, который позволит создавать запросы к данным в терминах объектной модели приложения и легко транслировать их в язык, понимаемый хранилищем данных (для ObjectSpaces и MS SQL Server это SQL).

Для обращения к источнику данных в ObjectSpaces используется специальный язык запросов – OPath. Синтаксис этого языка (отдаленно он напоминает XPath) позволяет выполнять запросы к источнику данных, основываясь на иерархии классов, используемых в приложении. В настоящее время OPath поддерживает следующий набор операторов (для операторов может использоваться синтаксис как C#, так и VB.NET):

Оператор в C# стиле

Оператор в VB стиле

Описание

.

[]

.

()

Операторы группировки используются для разделения свойств и группировки выражений. Например:Customer[CustomerID=’alfki’].Orders.ShipDate>#11/12/2003#

!

not

Логическое отрицание. not (Customer[CustomerID=’alfki’])

*

/

%

*

/

MOD

Умножение, деление, получение модуля

+

-

+

-

Сложение, вычитание

<

>

<=

>=

<

>

<=

>=

Сравнение двух значенийCustomer.CreateDate > #12/09/2002#

=

!=

==

=

<>

==

Равенство двух значений

&&

||

and

or

Customer.Region = ‘ru’ || Customer.Region = ‘en’

^

^

Символ ^ используется для обозначений родитель/потомок. В случае использования оператора ^ следующие два выражения эквивалентны:Orders.OrderDetail[^.OrderDate > #1/1/2003#]Orders.[OrderDate > #1/1/2003#]

ObjectSpace

При работе с сохраняемыми объектами нам нужны следующие возможности – загрузка сохраненных объектов, отслеживание состояния и возврат изменений обратно, в базу данных. Класс ObjectSpace объединяет в себе все эти возможности. Рассмотрим отдельные моменты работы с этим классом.

Создание экземпляра ObjectSpace

Для создания экземпляра ObjectSpace нужно иметь три схемы – RSD, OSD и MSD (при желании их можно скомбинировать в одном XML-файле), а также экземпляр SqlConnection для взаимодействия с источником данных.

// Создание экземпляра класса ObjectSpaces

using (SqlConnection conn = new SqlConnection(

 "Data Source=tim; Integrated Security=SSPI; Database=northwind"))

{

 ObjectSpace os = new ObjectSpace("map.xml", conn);



 // Работаем с os. Явно открывать подключение SqlConnection не обязательно.

 // Это происходит автоматически.

}

Запрос к источнику данных

После инициализации экземпляра ObjectSpace можно обратиться к источнику данных. Для этого у класса ObjectSpace есть три метода GetObject, GetObjectReader, GetObjectSet которые позволяют получать данные в виде трех различных форм – одиночный объект, курсор или список. 

 // Определим “сохраняемые” объекты, которые будем использовать в дальнейшем

 public class Customer

 {

  public string CustomerID;

  public string Name;

  public string Company;

  public string Phone;

  public string Fax;

  public ArrayList Orders = new ArrayList();

 }



 public class Order

 {

  private int _orderID = 0;



  public int OrderID

  {

   get { return _orderID; }

  }



  public DateTime OrderDate;

  public DateTime RequiredDate;

  public DateTime ShippedDate;

  public Decimal Freight;

  public int EmployeeID;

  public Customer Customer;

}



// Извлекаем объект Customer (включая подчиненное свойство Orders)

// на основе OPath-запроса (City='Berlin' && Orders.OrderDate < #1998.10.10#).

// Для каждого экземпляра класса Customer загружается свойство “Orders”.

Customer cust = (Customer)os.GetObject(typeof(Customer), 

   "City='Berlin' && Orders.OrderDate < #1998.10.10#", “Orders”);

Во что выливается вызов приведенного выше метода os.GetObject? Используя Profiler из MS SQL Server, можно увидеть, что в БД будет выполнен следующий SQL-запрос (отформатирован для приведения в более “читаемый” вид):

exec sp_executesql 

N'select Customers.[CustomerID], 

   Customers.[CompanyName], 

   Customers.[ContactName], 

   Customers.[City], 

   Customers.[Phone] 

  from [Northwind].[dbo].[Customers] as Customers 

  where ((Customers.[City]) = (@p0)) 

    AND (EXISTS(

       select Orders.[OrderID], Orders.[CustomerID] 

        from [Northwind].[dbo].[Orders] as Orders 

        where ((Customers.[CustomerID]) = (Orders.[CustomerID]))

         AND ((Orders.[OrderDate]) > (@p1)))) 

 order by 1;



select Customers.[CustomerID], 

  Orders.[OrderID], 

  Orders.[CustomerID], 

  Orders.[RequiredDate], 

  Orders.[ShippedDate], 

  Orders.[OrderDate] 

 from [Northwind].[dbo].[Customers] as Customers, 

    [Northwind].[dbo].[Orders] as Orders 

 where (((Customers.[City]) = (@p0)) 

  AND (EXISTS(

   select Orders.[OrderID], Orders.[CustomerID]

    from [Northwind].[dbo].[Orders] as Orders 

    where ((Customers.[CustomerID]) = (Orders.[CustomerID]))

     AND ((Orders.[OrderDate])>(@p1)) )))

     AND ((Customers.[CustomerID])=(Orders.[CustomerID])) 

 order by 1, 2, 3 ;', 



N'@p0 nvarchar(6),@p1 datetime', @p0 = N'Berlin', 

 @p1 = 'Oct 10 1998 12:00:00:000AM'

Создание записей в базе данных

Одно из больших преимуществ в использовании ObjectSpaces состоит в том, что для добавления объекту свойств “сохраняемости” его не надо специальным образом модифицировать (наследовать от специального базового класса, специальным образом размечать свойства или поля). Подобная прозрачность реализации ObjectSpaces дает преимущества в использовании.

// Работа с объектами Customer и Orders не зависит 

// от того, используется ObjectSpaces или нет

Customer cust = new Customer();

Order ord = new Order();



cust.Id = "ALFQI";

cust.Name = "MyName";

cust.Company = "MyCompany";

cust.Phone = "MyPhone";

cust.Fax = "MyFax";



ord.Customer = cust;

ord.OrderDate = DateTime.Now;

ord.ShippedDate = DateTime.Now;

ord.RequiredDate = DateTime.Now;

cust.Orders.Add(ord);



// Перед сохранением объектов необходимо поместить их в контекст 

// ObjectSpaces. Флаг InitialState.Inserted показывает, что мы добавляем новую 

// запись в базу данных

os.StartTracking(ord, InitialState.Inserted);

os.StartTracking(cust, InitialState.Inserted);



// Сохраняем экземпляр класса Customer. 

// Параметр PersistenceOptions(Depth.ObjectGraph) сообщает, 

// что будет сохранен весь граф объектов.

os.PersistChanges(cust, new PersistenceOptions(Depth.ObjectGraph));

Удаление записей с использованием ObjectSpaces

Существующая версия ObjectSpaces поддерживает удаление объектов только в том случае, если они ранее были добавлены в контекст ObjectSpaces. 

ПРИМЕЧАНИЕ

Для удаления объекта из базы данных его необходимо предварительно добавить в контекст ObjectSpaces. Это можно сделать, используя методы GetObject, GetObjectReader, GetObjectSet класса ObjectSpace, или добавить объект в контекст самостоятельно с помощью метода StartTracking



Customer cust = new Customer();

cust.Id = "ALFQI";



// Перед операцией над объектом необходимо поместить его в контекст 

// ObjectSpaces. Флаг InitialState.Unchanged показывает, что объект ранее

// был сохранен в базе данных

os.StartTracking(cust, InitialState.Unchanged);



// Помечаем экземпляр класса Customer как удаляемый. 

os.MarkForDeletion(cust);



// Сохраняем изменения в базе данных

os.PersistChanges(cust);

Отложенная загрузка данных

Отложенная загрузка данных – это очень полезная возможность, реализованная в ObjectSpaces. Правда, использование этой функциональности омрачается ее недостаточной “прозрачностью”. Это значит, что в случае, когда необходимо подгружать зависимые классы по требованию, придется модифицировать исходный код. К счастью, модификации незначительны. 

public class Customer

{

 public string CustomerID;

 public string Name;

 public string Company;

 public string Phone;

 public string Fax;



 // Для отложенной загрузки списка заказов необходимо перейти 

 // от использования ArrayList к использованию специального класса из 

 // ObjectSpaces – ObjectList.

 public ObjectList Orders = new ObjectList();

}



public class Order

{

 private int _orderID = 0;



 public int OrderID

 {

  get {return _orderID;}

 }



 public DateTime OrderDate;

 public DateTime RequiredDate;

 public DateTime ShippedDate;

 public Decimal Freight;

 public int EmployeeID;



 // Для отложенной загрузки класса Customer, мы меняем тип поля с Customer 

 // на ObjectHolder. Именно ObjectHolder будет отвечать за подгрузку нужных 

 // данных.

 private ObjectHolder _customer = new ObjectHolder();



 public Customer Customer

 {

  get {return (Customer) _customer.InnerObject;}

  set {_customer.InnerObject = value;}

 }

}

Кроме изменения кода приложения, отложенную загрузку свойств следует объявить в OSD-схеме. Для этого нужно добавить в описание полей специальный атрибут LazyLoad=”true”.

<!-- Фрагмент OSD схемы -->

<osd:Class Name="Rsdn.Samples.Northwind.Customer">

 <osd:Member Name="CustomerID" Key="true" />

 <osd:Member Name="Company" />

 <osd:Member Name="Name" />

 <osd:Member Name="Phone" />

 <osd:Member Name="Orders" LazyLoad=”true” />

</osd:Class>

<osd:Class Name="Rsdn.Samples.Northwind.Order">

 <osd:Member Name="_orderID" KeyType="AutoIncrement" 

       Hidden="false" Key="true" Alias="OrderID" />

 <osd:Member Name="OrderDate" />

 <osd:Member Name="RequiredDate" />

 <osd:Member Name="ShippedDate" />

 <osd:Member Name="EmployeeID" />

 <osd:Member Name="Freight" />

 <osd:Member Name="_customer" Alias=”Customer” LazyLoad=”true” />

</osd:Class>

После этого можно работать с восстановленным объектом как обычно:

using (SqlConnection conn = new SqlConnection(

 "Data Source=tim; Integrated Security=SSPI; Database=northwind"))

{

 ObjectSpace os = new ObjectSpace("map.xml", conn);



 Customer cust = (Customer)os.GetObject(typeof(Customer), 

  "CustomerID=’alfki’");



 // Список заказов загрузится при первом обращении

 foreach (Order order in cust.Orders)

 {

  Console.WriteLine(“Customer: {0}, OrderDate: {1}”, 

   order.Customer.Name, order.OrderDate);

 }

}

Метод

Описание

BeginTransaction, Commit, Rollback

Управление транзакциями. Стоит обратить внимание, что метод Rollback не откатывает изменения в сохраняемых объектах, поэтому возможны ситуации, когда информация в БД и информация в сохраняемых объектах окажутся несогласоваными. Поэтому, во избежание конфликтов, рекомендуется после Rollback создавать новый экземпляр ObjectSpaces.

GetObject

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

GetObjectReader

Получить из базы данных объекты через курсор, используя семантику, аналогичную используемой при работе с IDataReader.

GetObjectSet

Получить объекты из БД в виде единого массива. В отличии от ArrayList, класс ObjectSet предоставляет дополнительные возможности отслеживания оригинальных значений, передачи изменений через Remoting и некоторые другие.

PersistChanges

Сохранить измененный объект в БД.

MarkForDeletion

Пометить объект для удаления. Реальное удаление происходит при вызове PersistChanges.

Resync

Синхронизировать состочние объекта с информацией из БД.

StartTracking

“Пометить” объект как сохраняемый. Кроме текущих значений, в контексте сохраняется и состояние объекта (новый/измененный/удаленный/без изменений)

Дополнительные возможности ObjectSpaces

Чтение данных с использованием DbObjectReader

В отдельных случаях использование класса ObjectSpace может оказаться избыточным или неудобным. Например, если для доступа к базе данных необходимо использовать хранимые процедуры, большая часть функциональности ObjectSpaces окажется ненужной. Но и для подобных ситуаций в ObjectSpaces есть свое решение. Если требуется извлекать из произвольного источника данных информацию в виде объектов приложения, можно использовать класс DbObjectReader. Выступая как тонкая прослойка между ADO.NET-курсором (IDataReader) и классами приложения, DbObjectReader позволяет загружать сохраняемые объекты из источников данных, которые не поддерживаются ObjectSpaces напрямую.

public static void Main()

{

 DataTable table = new DataTable();



 table.Columns.Add("CustomerID", typeof(int));

 table.Columns.Add("CompanyName", typeof(string));

 table.Columns.Add("ContactName", typeof(string));

 table.Columns.Add("Phone", typeof(string));

 table.Rows.Add(new object[] { 1, "MyCompany", "MyCustomer", "222 33 22" });



 using (IDataReader reader = table.GetDataReader())

 {

  DbObjectReader objectReader = new DbObjectReader(reader,

   typeof(Customer), new MappingSchema("map.xml"));

  while (objectReader.Read())

  {

   Customer cust = (Customer)objectReader.Current;

   Console.WriteLine(cust.Name);

  }

 }

}

ObjectEngine

Класс ObjectEngine лежит в основе ObjectSpaces и реализует механизмы взаимодействия с источником данных. В большинстве случаев ObjectEngine напрямую не используется, но в ситуациях, когда необходимо выполнить OPath-запрос или сохранить объект в БД в обход основной функциональности ObjectSpaces и с минимальными издержками – использование ObjectEngine может пригодиться.

// Небольшой пример использования функциональности ObjectEngine

public static void Main()

{

 using (SqlConnection conn = new SqlConnection(

  "Data Source=tim; Integrated Security=SSPI; Database=northwind"))

 {

  conn.Open();



  // Учитывая, что ObjectEngine – это “низкоуровневый” класс, некоторую часть 

  // подготовительной работы приходится выполнять самостоятельно.

  ObjectContext context = 

   new CommonObjectContext(new ObjectSchema("osd.xml"));

  MappingSchema msd = new MappingSchema("map.xml");

  ObjectSchema osd = new ObjectSchema("osd.xml");

  ObjectSources sources = new ObjectSources();



  sources.Add("NorthwindRSD", conn);



  // Создаем OPath запрос и читаем данные из БД

  ObjectExpression expr = OPath.Parse(

    new ObjectQuery(typeof(Customer), "", ""), osd);



  // Еще одна издержка ObjectEngine – перед использованием OPath 

  // запрос надо “компилировать”.

  CompiledQuery query = expr.Compile(msd);

  Customer cust = null;



  // Выполняем OPath-запрос, используя “объектный” курсор.

  using (ObjectReader reader = 

   ObjectEngine.GetObjectReader(sources, context, query, new object[] { }))

  {

   while (reader.Read())

   {

    cust = (Customer)reader.Current;



    Console.WriteLine(cust.Name);

   }

  }



  // Cоздаем объект и сохраняем его в источнике данных

  cust = new Customer();

  cust.CustomerID = "alfq";

  cust.Name = "MyName";

  cust.Phone = "MyPhone";

  cust.Company = "MyComp";

  context.Add(cust, ObjectState.Inserted);



  ObjectEngine.PersistChanges(msd, sources, context, 

   new object[] { cust }, PersistenceOptions.Default);

 }

}

Расширения ObjectSpaces

Использование нескольких XML-схем для описания структуры классов приложения, реляционной структуры БД, а кроме того еще и Mapping-схемы, не может не удручать. Конечно, в финальной версии .NET Framework 1.2 возможности визуального проектирования этих схем должны обязательно появиться, но пока их нет, можно воспользоваться сторонними средствами. Одно из таких средств входит в пример ObjectSpacesPDCSamples.zip (файл можно найти на http://www.gotdotnet.com). 

В состав этого примера входит специальная утилита для создания всех необходимых XML-схем (рисунок 4). 











Рисунок 4. Microsoft ObjectSpaces Mapper Utility.

Кроме этого, в данный пример входит реализация класса ObjectPersistence. Этот класс обладает одной характерной особенностью – он скрывает в себе не только создание XML-описаний, но и создание необходимой базы данных. Рассмотрим простейший пример использования ObjectPersistence.

using System;

using Microsoft.ObjectSpaces.ObjectPersistence;



class ObjectPersistenceDemo

{

 // Исходный код класса ObjectPersistence также доступен в рамках примера

 static ObjectPersistence op = new 

  ObjectPersistence("Data Source=local; Integrated Security=true;", 

  "Persistence");



 static void Main(string[] args)

 {

  Customer c = new Customer();



  // Ищем заказчика в базе данных 

  c = (Customer)op.LoadObject(typeof(Customer), "CustomerID = 'alfki'");

  if (c == null)

  {

   c = new Customer("alfki");

   c.Comments = "New Customer";

  }

  else

  {

   c.Comments = "Old Customer";

  }

  

  // Сохраняем изменения. 

  // Если база данных/таблица еще не созданы, то это произойдет сейчас

  op.Persist(c);

 }

}

Класс ObjectPersistence спроектирован таким образом, что для его использования не обязательно предварительно создавать базу данных, настраивать XML-схемы данных – все это делается внутри реализации ObjectPersistence. Так, в приведенном выше примере на SQL Server будет создана база данных Persistence, и в нее будет добавлена таблица с именем Customer. Конечно, не в каждом проекте можно допустить подобные вольности со стороны библиотеки доступа к данным, но для простейших реализаций – это замечательная возможность скрыть ненужные детали.

Итог

Технологии доступа к данным в .NET Framework 1.2 содержат множество полезных нововведений, но если для ADO.NET это скорее эволюционные изменения, связанные с простым расширением библиотеки, то ObjectSpaces является совершенно новым продуктом, который может кардинальным образом изменить подход к работе с данными. Конечно, в настоящий момент работа над библиотекой еще далека от завершения. К моменту выхода VisualStudio «Whidbey» мы сможем увидеть в ней массу изменений, начиная с использования generics и расширения возможностей OPath, и заканчивая DML-операторами для удаления объектов без предварительного их извлечения.