Category Archives: Uncategorized

Введение в XPO и ORM

Эта статья рассчитана не только на тех кто испытывает затруднения с пониманием нюансов касающихся ХРО, но и даже тех кто и вовсе мало знаком с термином ORM. Я постараюсь наглядно объяснить что к чему.

Как сказано в умных статьях, ORM это способ абстрагироваться от SQL запросов при работе с базами данных и оперировать данными используя объектно ориентированный подход. Что же это означает в переводе на простой язык?

Для начала, разберемся что такое есть вообще этот объектно ориентированный подход работы с данными. Под этим обычно имеется ввиду этот принцип ООП – любой объект или явление можно смоделировать набором свойств, описывающих существенные параметры. В сущности, любая таблица как раз и представляет из себя набор свойств, описывающих объект.

Допустим, у нас имеется простая база данных содержащая две связанные таблицы – Автор и Книжка. Таблица Автор описывает автора книги и имеет поля Имя, Фамилия. Таблица Книжка описывает книгу, и имеет поля Название, и Автор. Этой информации может быть достаточно для каталога домашней библиотеки, например.

Нет ничего проще представить описанные данные в виде классов:

public class Author {

public string FirstName { get; set; }

public string LastName { get; set; }

}

public class Book {

public string Title { get; set; }

public Author Author { get; set; }

}

Описать то просто, но это еще не ORM. Нам теперь просто обрабатывать данные, но все еще необходимо писать SQL запросы чтобы загружать данные из БД, создавать экземпляры классов и вручную заполнять поля. ORM – это библиотека которая заботится о выполнении перечисленных выше и многих других задач, позволяя программисту на время забыть о SQL запросах и разных особенностей работы с базами данных.

Что ж, пришло время разобраться на примере ХРО, что же такого дают нам ORM и не проще ли обойтись без них и делать все вручную. Но прежде чем приступить непосредственно к загрузке данных, я должен рассказать о паре вещей.

Первый вопрос который необходимо решить, это каким образом связать классы с таблицами в базе данных, так чтобы ХРО знал из каких таблиц загружать данными. «Было бы неплохо если бы имя таблицы можно было бы задать в имени класса», заметит искушенный читатель. Что ж, не буду скрывать, по умолчанию ХРО действительно использует имя класса чтобы ассоциировть его с таблицей. В это случае, имя класса должно полностью соответствовать имени таблицы.

Но для того чтобы ХРО понимал какие классы можно использовать для запросов к БД, а какие нельзя, ввели специальный атрибут PersistentAttribute. ХРО использует только те классы, которые помечены этим атрибутом. Этот атрибут к тому же позволяет указать имя таблицы к которой привязан класс. Таким образом, если имена таблиц в базе данных имеют префиксы или вообще представляют из себя абракадабру, можно использовать это свойство атрибута чтобы дать классу человеческое имя и привязать его к нужной таблице.

[Persistent]

public class Author {

public string FirstName { get; set; }

public string LastName { get; set; }

}

ХРО считает все публичные и открытые для записи свойства персистного класса персистными (будем для краткости использовать этот новый термин – персистныйJ). Однако атрибут PersistentAttribute можно применять к свойствам и закрытым полям класса. Обычно это делают когда имя свойства отличается от имени колонки в таблице (я забыл упомянуть что имена персистных свойств ХРО использует для привязки их к колонкам по той же схеме что и имя класса для привязки его к таблице), реже – для того чтобы сделать закрытое свойство или поле персистным.

Если база данных соответствует 3-й нормальной форме (признаться, ХРО накладывает такое требование к базе данных), то у каждой таблицы должен быть первичный ключ. В нашем примере мы не добавили его в описание класса за ненадобностью. Однако, для ХРО он необходим и играет ключевую роль (простите за каламбур). Персистное свойство или поле класса привязанное к первичному ключу необходимо пометить атрибутом KeyAttribute. Конструктор этого атрибута имеет параметр типа Boolean, который позволяет указать является ли ключ автоинкрементным. Этот параметр важен при добавлении строк в таблицу. Если он false, то значит перед сохранением нового объекта необходимо самостоятельно присвоить значение ключевому свойству. Если же параметр auto increment = true, тогда после сохранения нового объекта ХРО присвоит этому свойству идентификатор который база данных присвоила строке.

[Persistent]

public class Author {

  [Persistent, Key(true)]

  int ID;

  public string FirstName { get; set; }

  public string LastName { get; set; }

}

У кого нибудь возникли вопросы по поводу свойства Author в классе Book? J Что ж, наблюдательные вы мои, действительно, тип этого свойства ставит под сомнение возможность его привязки к соответствующей колонке в таблице. Получается, что если мы хотим привязать его ко внешнему ключу (AuthorID), то нам придется изменить его тип? Это логично, но тогда мы теряем удобный способ узнать имя автора имея экземпляр книги. К счастью, ХРО позволяет нам использовать персистный класс как тип свойства. В таком случае, ХРО полагает что тип колонки в базе данных соответствует типу первичного ключа класса Author, и что внешний ключ AuthorID ссылается на первичный ключ в таблице Author. Это самый распространенный способ связывания таблиц. Будем считать что в нашей базе данных он и используется. В таком случае, нам необходимо указать что свойство Author привязано с внешнему ключу. Используем для этого PersistentAttribute.

[Persistent(“AuthorID”)]

public Author Author { get; set; }

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

Мы только что проделали кучу работы, но ни на воробьиный скок не приблизились нас к нашей цели – загрузить данные используя хваленый ООП подход. Пора мне пускать в ход секретное оружие, покуда читатели не плюнули и не ушли писать SQL запросы J

Все операции с базой данных можно произвести используя класс DevExpress.XPO.Session. Например, метод Save запишет данные в БД. Рассмотрим вот такой код:

Session session = new
Session();

session.ConnectionString = @”data source=.\sqlexpress2008;initial catalog=IntroducingXPO;integrated security=sspi”;

session.Save(new Author() { FirstName = “Lev”, LastName = “Tolstoy” });

При вызове метода Save, ХРО подключается к базе данных. Если подключение произошло, ХРО проверяет наличие базы данных IntroducingXPO. Если такой БД нету, ХРО создает новую базу данных. Затем ХРО проверяет наличие таблицы Author и наличие в этой таблице колонок соответствующих каждому персистному свойству. При отсутствие таковых, ХРО добавляет недостающие элементы, будь то таблица или колонка. Ну и наконец в таблицу Author вставляется новая запись и все колонки заполняются значениями соответствующих свойств.

А теперь оценить объем работы проделанный вызовом всего лишь одного метода! Вот она мощь ORM J

А сейчас немного занудства J Мы использовали свойство Session.ConnectionString для того чтобы подключить экземпляр сессии к базе данных. Такой подход приемлем для небольшой утилиты, которая довольствуется единственным экземпляром сессии. Но будет если мы будем разрабатывать большое коммерческое приложение, в котором будет множество модулей, и мы захотим чтобы каждый модуль имел свой экземпляр сессии. Если мы будем подключать каждый экземпляр сессии используя свойство ConnectionString, у нас возникнут две проблемы. Во первых, каждая сессия создаст новое подключение к базе данных, а это чрезмерный расход ресурсов. Во вторых, если мы решим подключиться к другой базе данных, придется исправлять строку подключения во всех модулях.

Вторая проблема конечно же раздутая до слона муха, и легко решается J Я упомянул ее тут для солидности. А вот чрезмерное количество соединений – это проблема от которой необходимо избавляться любыми способами. К счастью, ХРО позволяет настроить соединение один раз и передавать его как параметр конструктора класса Session. Вот как это выглядит:

IDataLayer dataLayer = XpoDefault.GetDataLayer(MSSqlConnectionProvider.GetConnectionString(@”.\sqlexpress2008″, “IntroducingXPO”), AutoCreateOption.DatabaseAndSchema);

Session session = new Session(dataLayer);

– Ого-го! Что означает эта куча умных словечек? Откуда они вдруг возникли?

– А вот так вот работает ХРО J Вся эта сложная конструкция выполнялась под капотом когда мы просто присваивали строку подключения экземпляру сессии. Не спешите, однако, записывать этот код в блокнотик. Я сейчас нарисую небольшую схемку и растолкую как все эти классы взаимодействуют. Усвоив принцип их взаимодействия, можно будет писать этот код в любое время не подглядывая в блокнотик.

2013-08-01_1443

И так, я пол часа рисовал эту чертову диаграмму, так что будет обидно если она не прольет ни капли света на взаимодействие компонентов ХРО. Каждый прямоугольник в этой схеме символизирует уровень абстракции. Сейчас попытаюсь пояснить что я имею ввиду, а затем опишу подробно роль каждого уровня.

Наверняка все хотя бы примерно представляют как происходит взаимодействие с базой данных. В общих чертах, мы создаем подключение, выполняем некий SQL запрос и забираем полученные данные. Можно написать класс, который будет предоставлять публичные функции GetAuthors и GetBooks. Внутри функций мы будем создвать подключение, выполнять SQL запрос, забирать данные и заполнять ими свойства объектов. В остальных частях программы мы будем просто использовать функции GetAuthors и GetBooks. Вот это я и имею ввиду под абстрагированием. Внутри нашего класса мы можем реализовать и использовать такие функции как CreateConnection, CreateCommand. Мы можем выделить эти функции в отдельный класс. Таким образом, у нас образуются уже два уровня абстрагирования. На одном мы получаем данные не заботясь о том с какой базой данных работать. На другом мы реализуем запросы к конкретной базе данных получая задания от верхнего уровня.

ХРО делится на множество уровней абстракции, если рассматривать его под более сильным микроскопом. Но для ознакомления достаточно знать три основных уровня.

Session – Самый верхний уровень абстракции, которые предоставляет программисту удобные метода для загрузки объектов и сохранения изменений. На этом уровне, программист работает только с объектами, не задумываясь о том что происходит на нижнем уровне. Сессия преобразует информацию полученную через объекты в массив экземпляров SelectStatement или ModificationStatement в зависивости от того какой метод класса Session выполняется в данный момент. SelectStatement и ModificationStatement позволяют описать запросы к базе данных используя специальный объектный язык. Из этих запросов ХРО будет строить SQL команды на следующем уровне абстракции.

Data Layer – Это следующий уровень абстракции, который принимает от сессии запросы построенные с помощью специального объектного языка, упомянутого вкратце в предудущем абзаце. Его роль заключается не только в передаче этих запросов конкретному SQL провайдеру. Зачастую, на этом уровне реализуется дополнительная логика – журналирование запросов, обработка пультипоточных запросов.

Data Store представляет собой SQL провайдер, заточенный под конкретную базу данных. Он преобразует SelectStatement и ModificationStatement запросы в SQL команды в соответствии с требованиями базы данных. Список БД поддерживаемых на данный момент опубликован на нашем сайте в разделе Database Engines Supported by XPO. Список соответствующих XPO провайдеров находится в разделе Database Systems Supported by XPO. Кроме того, опытный программист может попытаться самостоятельно написать провайдер к базе данных которая не поддерживается из коробки. Это очень трудоемкая задача которая требует глубоких знаний ХРО, ADO .NET и реляционных баз данных, включая особенности той БД для которой нужно написать провайдер.

Но вернемся к нашему коду. Теперь становится немножечко ясно что мы передаем конструктору сессии экземпляр Data Layer с которым мы хотим чтобы она работала. Однако предыдущая строчка все еще требует пояснений. Разложим ее на два действия, дабы не оставлять ничего скрытым под капотом и было проще понять.

IDataStore dataStore = XpoDefault.GetConnectionProvider(MSSqlConnectionProvider.GetConnectionString(@”.\sqlexpress2008″, “IntroducingXPO”), AutoCreateOption.DatabaseAndSchema);

IDataLayer dataLayer = new SimpleDataLayer(dataStore);

Session session = new Session(dataLayer);

Как видите – все очень просто. Создаем три основных слоя и надеваем их друг на друга как матрешки.

Первый слой – экземпляр IDataStore. Можно использовать непосредственно конструктор любого из провайдеров, а можно воспользоваться фабрикой XpoDefault.GetConnectionProvider. Этот метод создаст нужный провайдер проанализировав строку соединения, переданную в конструктор. Однако, не все строки соединения несут достаточно информации чтобы определить какой SQL провайдер нужно использовать. Поэтому в строку соединения рекомендуется добавлять параметр XpoProvider. Его значения описаны в этой статье – How to create a correct connection string for XPO providers? Еще один простой способ получить правильную строку подключения – использовать метод GetConnectionString у соответствующегоSQL провайдера.

Второй слой – IDataLayer. Существует два класса реализующих этот интерфейс: SimpleDataLayer и ThreadSafeDataLayer. При необходимости можно написать собственный класс расширяющий существующие.

Последний слой – Session. Экземпляр Session создается тогда когда необходимо произвести операции с базой данных. Можно создавать сколько угодно много экземпляров сессий, но главное не смешивать несколько экземпляров сессии в одной бизнес задаче. Иначе не избежать путаницы J

ХРО расширяемо на любом из описанных трех уровней. Любой из уровней можно заменить не изменяя остальные. При этом написанный ранее код будет оставаться работоспособным. Такой подход позволяет легко раcширять приложения написанные с помощью ХРО. Можно с легкостью мигрировать на другую версию SQL сервера просто заменив слой Data Store.

Вернемся же к нашему приложению, потому что мы уже морально готовы сделать UI и написать методы которые загружают данные и сохраняют изменения. Добавим на форму SplitContainer. В каждую панель – по DataGridView, чтобы редактировать список авторов и их книги.

Для того чтобы загрузить данные, достаточно написать вот такой простой код:

Session session = new Session(Program.DataLayer);

gridAuthors.DataSource = session.GetObjects(session.GetClassInfo<Author>(), null, new SortingCollection(), 0, false, false);

Однако, это не самый лучший способ. В WinForms приложениях рекомендуется использовать специальный класс – XPCollection. Во первых, он позволяет загрузить объекты не ломая голову над параметрами метода GetObjects. Во вторых, его поведение адаптировано под WinForms Binding, поэтому это очень полезный класс во всех отношениях.

Session session = new Session(Program.DataLayer);

gridAuthors.DataSource = new XPCollection<Author>(session);

Несмотря на что наш код теперь проще некуда, это все еще не предел возможного. XPCollection это компонент, и его можно использовать в дизайнере формы и UserControl! Добавим же его на форму и найдем свойство ObjectClassInfo. Это свойство поможет нам указать какой класс загрузить в коллекцию. Если нажать на кнопку расположенную справа от свойства, то откроется список который волшебным образом заполнен нашими персистными клссами. Мы выберем класс Author и запустим приложение.

К нашему разочарованию, в таблице нет никаких данных (на 64 битных машинах приложение может и вовсе не запуститься). Это произошло потому что мы не указали какую базу данных использовать, и ХРО по умолчанию решил использовать базу данных Microsoft Access. Если бы мы попытались добавить строчку и сохранить ее, то ХРО создал бы файл базы Access в корневом каталоге приложения. Но так как мы только запрашивали данные, хитрый ХРО смекнул что файл может даже и не понадобиться, и не стал его создавать.

У XPCollection нету свойства ConnectionString, но его можно привязать к объекту Session через соответствующее свойство. Session тоже компонент, его можно добавить на форму и использовать. Но как же в таком случае избежать необходимости каждый раз присваивать ConnectionString? Все предусмотрено. Вместо того чтобы хранить объект Data Layer в глобальной переменной приложения, его нужно присвоить глобальному свойству XpoDefault.DataLayer.

Сессия созданная в дизайн-тайме будет использовать именно это свойство для инициализации. Тогда наши настройки XPCollection будут выглядеть так:

Добавим возможность сохранять введенные пользователем данные в базу. Эта задача решается одной строчкой кода. Положите на форму кнопку, назовите Save и в обработчике клика напишите вот так:

Session.Save(Authors);

Если теперь добавлять новые строки или редактировать старые, все изменения будут записаны в базу после нажатия кнопки Save. Вот правда удаленные строки будут возвращаться на месть L Это потому что по умолчанию объект удаленный из XPCollection не считается уничтоженным. Поэтому, открываем дизайнер формы, и присваиваем свойству XPCollection.DeleteObjectsOnRemove значение true. Теперь если удалить строки из грида и сохранить изменения, то соответствующие строки будут удалены из таблицы в БД.

Теперь настало время придумать каким образом нам показывать во второй таблице список книг автора выделенного в первой таблице. Если бы мы пользовались DataSet, то все что нам нужно было бы это добавить связь между двумя таблицами, и привязать второй грид к детальной таблице используя свойство DataMember. А помните, как я сказал что XPCollection заточено под цели WinForms Binding? Вот сейчас это нам и поможет.

Однако прежде я предлагаю еще раз слегка усложнить наши персистные классы. Сейчас они у нас представляют собой так называемые безсессионные (session less) персистные объекты. Они вполне годятся для простых задач, но львиная доля волшебства ХРО теряется при их использовании. Я считаю что настало время переходить на новый уровень, и сделать все наи персистные классы наследниками класса XPBaseObject. Этот класс реализует множество полезных функций и не заменим в WinForms Binding сценариях.

public class Author :XPBaseObject {

public class Book :XPBaseObject {

Если мы сейчас же запустим приложение, то получим исключение: Type DxSample.Author does not have a session-specific constructor. Все логично, раз уж наши классы теперь крепко связаны с сессией ХРО, мы должны были добавить соответствующие конструкторы:

public Author(Session session) : base(session) { }

public Book(Session session) : base(session) { }

Кстати, конструкторы по умолчанию лучше не оставлять. Запомните этот хитрый трюк, он вам не раз поможет в будущем. Фокус заключается в том, что программисты нередко забывают указать экземпляр сессии при создании персистного класса или персистной коллекции. В таком случае, персистные объекты используют XpoDefault.Session. Это часто приводит к трудноанализируемым ошибкам вызываемым тем что две разные сессии используются в одной и той же задаче. Чтобы избежать этого, не добавляйте конструктор по умолчнию в персистный класс. В таком случае, если вы забудете указать сессию, то при выполнении получите исключение говорящее о том что конструктор по умолчанию не найден. Стек вызовов будет указывать прямехонько на то место в вашем коде где была допущена ошибка. Это весьма удобно.

Если вы теперь добавите или отредактируете какую нибудь строчку, а после переведете фокус на другую, вы можете заметить что данные сохраняются сами собой, хотя кнопку Save вы не нажимали. Это одна из функций особенностей сессии – сохранять изменения автоматически при окончании редактирования (вызове метода IEditableObject.EndEdit которые реализован в классе XPBaseObject). Иногда эта функция полезна, иногда же наоборот необходимо дать пользователю возможность самому решать когда сохранять изменения, или откатиться к первоначальному состоянию.

Для этой задачи в ХРО существует класс UnitOfWork. Он расширяет класс Session и я предлагаю заменить в наем приложении Session на UnitOfWork. То есть просто добавить UnitOfWork на форму, и присвоить его свойству XPCollection.Session. Мы только что подменили реализацию одного из уровней абстракции. Как я обещал выше, наш код должен работать без изменений. Однако быстрая проверка показывает обратное – вызов UnitOfWork.Save больше не сохраняет изменения в базу данных.

На самом деле, метод Save продолжает функционировать правильно. Дело в том что UnitOfWork работает в режиме транзакции. Он просто запоминает что объект был сохранен или удален, но никаких попыток записать изменения в базу данных не делает. Завершить транзакцию можно только с помощью метода CommitChanges или ReloadChangedObjects. Поэтому просто поправим наш код (вызов метода Save нам больше не нужен, потому что мы убедились что наши объекты теперь умеют взаимодействовать с WinForms Binding и сохраняют себя сами):

//UnitOfWork.Save(Authors);

UnitOfWork.CommitChanges();

Если бы мы редактировали свойства персистного класса не через WinForms Bindings, а напрямую из кода, то пришлось бы использовать метод Save чтобы изменения записались в базу данных после вызова CommitChanges. Но на самом деле, это тоже не обязательно было бы делать, будь наши персистные классы написаны по всем правилам J Класс XPBaseObject предоставляет метод SetPropertyValue, который рекомендуется исползовать в сеттере персистного свойства. Во первых, этот метод поднимает событие PropertyChanged, что является частью контракта INotifyPropertyChanged. Это очень важно для WinForms Binding. Во вторых, если выполнился метод SetPropertyValue, UnitOfWork запоминает что свойство объекта было изменено и когда выполнится метод CommitChanges, эти изменения будут записаны в базу данных. Предлагаю переписать все персистные свойства чтобы они полностью соответствовали рекомендованному шаблону:

public class Author :XPBaseObject {

public Author(Session session) : base(session) { }

  [Persistent, Key(true)]

  int ID;

  string fFirstName;

  public string FirstName {

    get { return fFirstName; }

    set { SetPropertyValue<string>(“FirstName”, ref fFirstName, value); }

   }


string fLastName;

  public string LastName {

    get { return fLastName; }

    set { SetPropertyValue<string>(“LastName”, ref fLastName, value); }

  }

}

public class Book :XPBaseObject {

  public Book(Session session) : base(session) { }

   [Persistent, Key(true)]


int ID;

  string fTitle;

  public string Title {

    get { return fTitle; }

    set { SetPropertyValue<string>(“Title”, ref fTitle, value); }

   }


Author fAuthor;

  [Persistent(“AuthorID”)]


public Author Author {

    get { return fAuthor; }

    set { SetPropertyValue<Author>(“Author”, ref fAuthor, value); }

   }

}

Пришла пора вернуться к нашей задаче – показывать во втором гриде книги выбранного автора. Для этой цели необходимо объявить ассоциацию между персистными классами. Чтобы это сделать, пометим свойство Book.Author атрибутом AssociationAttribute. В классе Author добавим свойство Books типа XPCollection, и тоже пометим его атрибутом AssociationAttribute. В конструкторы атрибутов необходимо передать имя ассоциации. Имя может быть произвольным, но необходимо соблюдать следующие правила. Обе части одной ссоциации (свойство ссылка и коллекция) должны иметь одинаковое имя. Две разных ассоциации не могут иметь одно и тоже имя.

public class Author :XPBaseObject {

  [Association(“Author-Books”)]

  public XPCollection<Book> Books {

    get { return GetCollection<Book>(“Books”); }

   }

public class Book :XPBaseObject {

   [Persistent(“AuthorID”), Association(“Author-Books”)]

  public Author Author {

    get { return fAuthor; }

    set { SetPropertyValue<Author>(“Author”, ref fAuthor, value); }

 }

В аксессоре свойства Books используется метод GetCollection. Этот метод позволяет не просто получить коллекцию объектов ассоциированных с данным а гораздо больше. Если уничтожить объект представляющий из себя книгу (метода XPBaseObject.Delete или Session.Delete([object])), то этот объект автоматически удалится изо всех ассоциативных коллекций. То же самое произойдет если изменить свойство Book.Author. Объект Book автоматически удалится изо всех ассоциативных коллекций старого автора, и добавится в ассоциативные коллекции связанные с новым автором.

Теперь мы можем привязать второй грид к той же самой коллекции авторов, и в списке свойства DataMember грида станет доступно свойство Books.

Ура, теперь мы можем отображать и редактировать детальные коллекции.

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

Ну как, кто нибудь уже нашел ошибку? J В общем ошибка заключается в том, что мы можем удалять записи из детальной коллекции, но даже если мы сохраним изменения, удаленные записи появятся вновь после перезагрузки приложения. Происходит так потому, что свойство XPCollection.DeleteObjectOnRemove по умолчанию равно false. Но в данном случае мы не можем просто так взять и поменять значение у этого свойства, так как детальные коллекции создаются внутри ХРО.

Многие программисты, сталкиваясь с этой проблемой, пытаются решить ее в лоб. Они рассуждают так: можно в аксессоре свойства коллекции изменить свойство XPCollection.DeleteObjectOnRemove. Я точно знаю, думает программист, что во всех сценариях моего приложения мне нужно чтобы объекты удаленные из детальной коллекции автоматически уничтожались.

[Association(“Author-Books”, typeof(Book))]

public XPCollection<Book> Books {

  get {

    XPCollection<Book> result = GetCollection<Book>(“Books”);

     result.DeleteObjectOnRemove = true;

    return result;

   }

}

На самом деле это ошибка, потому что речь идет об ассоциативной коллекции. Удаление из такой коллекции означает разрыв связи с другим объектом, а не физическое удаление из БД. Нарушать эту логику не стоит. Взять простой пример – пользователь добавил Льву Толстому книгу «Айболит», и неожиданно вспомнил что ее написал Корней Чуковский. «Ежкин кот», подумал пользователь, и перенес книгу из коллекции Толстого в коллекцию Чуковского. Но так как программист не предусмотрел такой сценарий и присвоил свойство DeleteObjectOnRemove в true у ассоциативной коллекции, то книга «Айболит» будет потеряна.

Самый простой и правильный способ это перехватить управление процессом удаления объекта, и сделать это правильно:

UnitOfWork.Delete(gridBooks.CurrentRow.DataBoundItem);

Главное не забыть про две вещи: когда используется UnitOfWork изменения записываются в базу только вызовом CommitChanges, и не забудте удалить неправильный код из свойства Books J.

На этом я пожалуй и завершу статью, а то получается слишком много букв. Это не всем нравится. Расскажу лишь на последок одну вещь. Один маленький абзац мне простят J

В этой статье используется подход к использованию ORM который получил название “code first”. Другими словами, мы сначала описали структуру данных используя классы, и позволили ORM создать для нас новую базу данных. Однако не редки случаи когда перед программистом стоит задача написать приложение некой базы данных, которая уже существует и менять ничего в ней нельзя, потому что она используется другими приложениями. В этом случае, можно не писать персистные классы вручную, и воспользоваться XPO Data Model Designer. Рассказывать об этом интересном зверьке я не буду, так как он и так интуитивно понятен и хорошо описан в нашей документации: Your First Data-Aware Application with XPO. Еще, я рекомендую почитать вот эти статьи:

XPO Best Practices – как надо делать

XPO Worst Practices – как не надо делать никогда

Leave a comment

August 1, 2013 · 10:18 am