- •Оглавление
- •Предисловие
- •Введение
- •Часть I Обзор Глава 1 "Расслоение" системы
- •Развитие модели слоев в корпоративных программных приложениях
- •Три основных слоя
- •Где должны функционировать слои
- •Глава 2 Организация бизнес-логики
- •Выбор типового решения
- •Глава 3 Объектные модели и реляционные базы данных
- •Архитектурные решения
- •Функциональные проблемы
- •Считывание данных
- •Взаимное отображение объектов и реляционных структур
- •Отображение связей
- •Наследование
- •Реализация отображения
- •Двойное отображение
- •Использование метаданных
- •Соединение с базой данных
- •Другие проблемы
- •Дополнительные источники информации
- •Глава 4 Представление данных в Web
- •Типовые решения представлений
- •Типовые решения входных контроллеров
- •Дополнительные источники информации
- •Глава 5 Управление параллельными заданиями
- •Проблемы параллелизма
- •Контексты выполнения
- •Изолированность и устойчивость данных
- •Стратегии блокирования
- •Предотвращение возможности несогласованного чтения данных
- •Разрешение взаимоблокировок
- •Транзакции
- •Типовые решения задачи обеспечения автономного параллелизма
- •Параллельные операции и серверы приложений
- •Дополнительные источники информации
- •Глава 6 Сеансы и состояния
- •В чем преимущество отсутствия "состояния"
- •Состояние сеанса
- •Глава 7 Стратегии распределенных вычислений
- •Соблазны модели распределенных объектов
- •Интерфейсы локального и удаленного вызова
- •Когда без распределения не обойтись
- •Сужение границ распределения
- •Интерфейсы распределения
- •Глава 8 Общая картина
- •Предметная область
- •Источник данных
- •Платформы и инструменты
- •Другие модели слоев
- •Часть II Типовые решения Глава 9 Представление бизнес-логики Сценарий транзакции (Transaction Script)
- •Модель предметной области (Domain Model)
- •Модуль таблицы (Table Module)
- •Слой служб (Service Layer)
- •Глава 10 Архитектурные типовые решения источников данных Шлюз таблицы данных (Table Data Gateway)
- •Шлюз записи данных (Row Data Gateway)
- •Активная запись (Active Record)
- •Преобразователь данных (Data Mapper)
- •Глава 11 Объектно-реляционные типовые решения, предназначенные для моделирования поведения Единица работы (Unit of Work)
- •Коллекция объектов (Identity Map)
- •Загрузка по требованию (Lazy Load)
- •Глава 12 Объектно-реляционные типовые решения, предназначенные для моделирования структуры Поле идентификации (Identity Field)
- •Отображение внешних ключей (Foreign Key Mapping)
- •Отображение с помощью таблицы ассоциаций (Association Table Mapping)
- •Отображение зависимых объектов (Dependent Mapping)
- •Внедренное значение (Embedded Value)
- •Сериализованный крупный объект (Serialized lob)
- •Наследование с одной таблицей (Single Table Inheritance)
- •Наследование с таблицами для каждого класса (Class Table Inheritance)
- •Наследование с таблицами для каждого конкретного класса (Concrete Table Inheritance)
- •Преобразователи наследования (Inheritance Mappers)
- •Глава 13 Типовые решения объектно-реляционного отображения с использованием метаданных Отображение метаданных (Metadata Mapping)
- •Объект запроса (Query Object)
- •Хранилище (Repository)
- •Глава 14 Типовые решения, предназначенные для представления данных в Web Модель-представление-контроллер (Model View Controller)
- •Контроллер страниц (Page Controller)
- •Контроллер запросов (Front Controller)
- •Представление по шаблону (Template View)
- •Представление с преобразованием (Transform View)
- •Двухэтапное представление (Two Step View)
- •Контроллер приложения (Application Controller)
- •Глава 15 Типовые решения распределенной обработки данных Интерфейс удаленного доступа (Remote Facade)
- •Объект переноса данных (Data Transfer Object)
- •Глава 16 Типовые решения для обработки задач автономного параллелизма Оптимистическая автономная блокировка (Optimistic Offline Lock)
- •Пессимистическая автономная блокировка (Pessimistic Offline Lock)
- •Блокировка с низкой степенью детализации (Coarse-Grained Lock)
- •Неявная блокировка (Implicit Lock)
- •Глава 17 Типовые решения для хранения состояния сеанса Сохранение состояния сеанса на стороне клиента (Client Session State)
- •Сохранение состояния сеанса на стороне сервера (Server Session State)
- •Сохранение состояния сеанса в базе данных (Database Session State)
- •Глава 18 Базовые типовые решения Шлюз (Gateway)
- •Преобразователь (Mapper)
- •Супертип слоя (Layer Supertype)
- •Отделенный интерфейс (Separated Interface)
- •Реестр (Registry)
- •Объект-значение (Value Object)
- •Деньги (Money)
- •Частный случай (Special Case)
- •Дополнительный модуль (Plugin)
- •Фиктивная служба (Service Stub)
- •Множество записей (Record Set)
- •Список типовых решений
- •Шпаргалка
- •Как управлять сложным потоком функций приложения?
- •Как взаимодействовать с базой данных?
- •Как избежать загрузки в оперативную память всего содержимого базы данных?
- •Как сохранить структуры наследования в реляционной базе данных?
Отображение внешних ключей (Foreign Key Mapping)
Отображает ассоциации между объектами на ссылки внешнего ключа между таблицами базы данных
Объекты могут ссылаться непосредственно друг на друга с помощью объектных ссылок (object references). Даже самая простая объектно-ориентированная система обязательно содержит группу объектов, связанных между собой всеми возможными и невозможными способами. Разумеется, при сохранении объектов в базе данных необходимо позаботиться и о сохранении всех ссылок. К сожалению, поскольку содержимое объектов специфично для конкретного экземпляра запущенной программы, сохранение значений "в чистом виде" ничего не даст. Данную проблему еще более усложняет тот факт, что объекты могут содержать коллекции ссылок на другие объекты. Подобная структура нарушает определение первой нормальной формы реляционных баз данных.
Типовое решение отображение внешних ключей отображает объектную ссылку на внешний ключ базы данных.
Принцип действия
Первая мысль, возникающая по поводу данной проблемы, — воспользоваться полем идентификации (Identity Field). Действительно, каждому объекту соответствует определенное значение ключа таблицы базы данных. Если два объекта связаны между собой некоторой ассоциацией, в базе данных ее можно заменить внешним ключом. Например, если вы сохраняете в базе данных запись об альбоме, в эту же строку можно поместить и идентификатор исполнителя, связанного с этим альбомом (рис. 12.1).
Рассмотренный случай довольно прост. А как же быть, если речь идет о ссылке на коллекцию объектов? В базе данных нельзя сохранить коллекцию ключей, поэтому направление ссылки придется изменить. Таким образом, имея коллекцию композиций альбома, необходимо поместить внешний ключ альбома в запись о каждой композиции (рис. 12.2 и 12.3). Основные сложности подобных ситуаций связаны с обновлением. В рамках обновления в коллекции композиций альбома могут произойти изменения, связанные с удалением или добавлением новых объектов. Как определить, какие изменения должны быть внесены в базу данных? Для этого можно воспользоваться тремя способами: удалением и вставкой, добавлением обратного указателя и операцией сравнения.
Рис. 12.1. Отображение коллекции на внешний ключ
Рис. 12.2. Отображение коллекции на внешний ключ
Первый, и наиболее простой, способ — удалить из базы данных все композиции, ссылающиеся на заданный альбом, и затем вставить в нее те композиции, которые на данный момент находятся в альбоме. На первый взгляд это звучит не очень умно, особенно если набор композиций на самом деле не изменился. Тем не менее эта логика проста в реализации и, кроме того, работает гораздо надежнее других способов. Следует также отметить, что данный способ можно применить только в том случае, если композиции связаны отображениями зависимых объектов (Dependent Mapping), а значит, являются полностью зависимыми от объекта-альбома и на них нельзя ссылаться извне.
Добавление обратного указателя подразумевает создание ссылки композиции на альбом, в результате чего связь между этими объектами становится двунаправленной. Разумеется, это изменяет объектную модель, однако позволяет значительно упростить обновление данных путем простого обновления однозначных полей "с другой стороны" ссылки.
Рис. 12.3. Классы и таблицы, связанные многозначной ссылкой
Если ни один из перечисленных способов вам не подойдет, попробуйте провести сравнение. В качестве эталона можно воспользоваться текущим состоянием базы данных или же множеством данных, которое было считано при первой загрузке объекта. В первом случае от вас требуется еще раз считать из базы данных коллекцию композиций, входящих в альбом, и затем сравнить их с текущим содержимым альбома. Все композиции считанного множества данных, которых нет в альбоме, должны быть удалены из базы данных, а все композиции альбома, которых нет в считанном множестве данных, должны быть добавлены в базу данных.
Чтобы сравнить текущее состояние коллекции с множеством данных, которое было считано при первой загрузке объекта, необходимо реализовать сохранение считываемых данных. Это более предпочтительный способ, поскольку он избавляет от необходимости дополнительного обращения к базе данных. Впрочем, обращение к базе данных может понадобиться, если вы используете оптимистическую автономную блокировку (Optimistic Offline Lock).
В более общем случае каждый объект, добавляемый в коллекцию, должен быть проверен на предмет того, является ли он новым объектом. Это можно установить по наличию ключа; если у добавленного объекта нет ключа, значит, он новый и его следует вставить в базу данных. Для реализации подобной проверки хорошо воспользоваться единицей работы (Unit of Work), поскольку она автоматически вставляет в базу данных новые объекты. В любом случае после выполнения этих действий в базе данных необходимо найти строку, соответствующую добавленной композиции, и обновить ее внешний ключ так, чтобы он указывал на текущий альбом.
При удалении композиции из содержимого альбома необходимо знать, была ли она перемещена в другой альбом, оставлена без альбома или окончательно удалена из базы данных. Если композиция была перемещена в другой альбом, ее внешний ключ должен быть обновлен при обновлении альбома. Если композиция была оставлена без альбома, ее внешнему ключу необходимо присвоить значение null. И наконец, если композиция была удалена, ее необходимо удалить вместе со всеми другими удаляемыми записями. Выполнять удаление значительно проще, если наличие обратной ссылки обязательно (как в нашем примере, когда каждая композиция должна принадлежать какому-нибудь альбому). В этом случае вам не придется определять, какие композиции были удалены из альбома, поскольку значения их внешних ключей будут обновлены при обновлении альбомов, в которые эти композиции были перемещены.
Обратная ссылка может быть неизменяемой (т.е. композиция не может быть перемещена в другой альбом). В этом случае добавление композиции в альбом всегда означает вставку строки в базу данных, а удаление из альбома — удаление соответствующей строки из базы данных. Это еще более упрощает обновление коллекций.
Работая с базой данных, необходимо следить за наличием циклических ссылок. Предположим, нужно загрузить заказ, ссылающийся на покупателя (который тоже будет загружен). Каждому покупателю соответствует множество платежей (сведения о котором тоже будут загружены), а каждый платеж ссылается на оплачиваемые им заказы. Последнее множество может включать в себя упомянутый заказ. Таким образом, будет загружен и этот заказ, после чего все повторяется снова и снова.
Во избежание подобной мешанины можно воспользоваться одним из двух способов создания объектов. Многие предпочитают применять конструкторы с аргументами, чтобы создаваемые объекты были полностью загружены данными. В этом случае придется прибегнуть к загрузке по требованию (Lazy Load), чтобы цикл загрузки прерывался в нужных местах. Если этого не сделать, вам грозит переполнение стека. Впрочем, если результаты тестирования оказались вполне приличными, без применения загрузки по требованию можно обойтись.
Второй возможный вариант — создать пустой объект и сразу же поместить его в коллекцию объектов (Identity Map). В этом случае при повторной ссылке на объект коллекция объектов сообщит, что он уже был загружен, и цикл будет прерван. Разумеется, объекты, создаваемые подобным образом, не заполнены данными, однако по окончании загрузки они будут содержать в себе все, что нужно. Это позволяет избежать описания частных случаев загрузки по требованию, что было бы крайне утомительно, если нужно всего лишь корректно загрузить объект.
Назначение
Отображение внешних ключей может применяться для моделирования практически всех видов связей между классами. Наиболее распространенный случай, когда отображение внешних ключей применить нельзя, — это связи типа "многие ко многим". Внешние ключи являются одномерными значениями, а из определения первой нормальной формы следует, что в одном поле нельзя хранить множественные значения внешних ключей. В этом случае вместо отображения внешних ключей необходимо воспользоваться отображением с помощью таблицы ассоциаций (Association Table Mapping).
Если у вас есть поле коллекции без обратного указателя, подумайте о том, не сделать ли "множественную сторону" ссылки отображением зависимых объектов. Это значительно упростит обработку коллекции.
Если связанный объект является объектом-значением (Value Object), вместо отображения внешних ключей следует воспользоваться внедренным значением (Embedded Value).
Пример: однозначная ссылка (Java)
Рассмотрим наиболее простой случай, когда объект-альбом ссылается на своего исполнителя.
class Artist...
private String name;
public Artist(Long ID, String name) {
super(ID);
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
class Album...
private String title;
private Artist artist;
public Album(Long ID, String title, Artist artist) {
super(ID);
this.title = title;
this.artist = artist;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Artist getArtist() {
return artist;
}
public void setArtist(Artist artist) {
this.artist = artist;
}
Процесс загрузки альбома показан на рис. 12.4. Когда преобразователь AlbumMapper получает указание загрузить альбом, он выполняет запрос к базе данных и возвращает результирующее множество данных, соответствующее заданному альбому. Затем преобразователь выполняет запрос к результирующему множеству данных и извлекает оттуда значение внешнего ключа исполнителя. Теперь преобразователь может загрузить объект альбома найденными значениями. Если объект исполнителя уже был загружен в память, он будет извлечен из кэша; в противном случае он будет загружен из базы данных так, как это делалось для альбома.
Рис. 12.4. Процесс загрузки однозначного поля
Для работы с коллекцией объектов метод поиска применяет абстрактное поведение.
class AlbumMapper...
public Album find(Long id) {
return (Album) abstractFind(id);
}
protected String findStatement() {
return "SELECT ID, title, artistID FROM albums WHERE ID = ?";
}
class AbstractMapper...
abstract protected String findStatement();
protected DomainObject abstractFind(Long id) {
DomainObject result = (DomainObject) loadedMap.get(id);
if (result != null) return result;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
stmt = DB.prepare(findStatement());
stmt.setLong(1, id.longValue());
rs = stmt.executeQuery();
rs.next();
result = load(rs);
return result;
} catch (SQLException e) {
throw new ApplicationException(e);
} finally {cleanUp(stmt, rs);}
}
private Map loadedMap = new HashMap();
Метод поиска вызывает метод загрузки, чтобы выполнить фактическую загрузку данных в объект альбома.
class AbstractMapper...
protected DomainObject load(ResultSet rs) throws SQLException {
Long id = new Long(rs.getLong(1));
if (loadedMap.containsKey(id)) return (DomainObject) loadedMap.get(id);
DomainObject result = doLoad(id, rs);
doRegister(id, result);
return result;
}
protected void doRegister(Long id, DomainObject result) {
Assert.isFalse(loadedMap.containsKey(id));
loadedMap.put(id, result);
}
abstract protected DomainObject doLoad(Long id, ResultSet rs) throws SQLException;
class AlbumMapper...
protected DomainObject doLoad(Long id, ResultSet rs) throws SQLException {
String title = rs.getString(2);
long artistID = rs.getLong(3);
Artist artist = MapperRegistry.artist().find(artistID);
Album result = new Album(id, title, artist);
return result;
}
Для обновления альбома преобразователь извлекает значение внешнего ключа его исполнителя.
class AbstractMapper...
abstract public void update(DomainObject arg);
class AlbumMapper...
public void update(DomainObject arg) {
PreparedStatement statement = null;
try {
statement = DB.prepare(
"UPDATE albums SET title = ?, artistID = ? WHERE id = ?");
statement.setLong(3, arg.getID().longValue());
Album album = (Album) arg;
statement.setString(1, album.getTitle());
statement.setLong(2, album.getArtist().getID().longValue());
statement.execute();
} catch (SQLException e) {
throw new ApplicationException(e);
} finally {
cleanUp(statement);
}
}
Пример: многотабличный поиск (Java)
Обычно область действия SQL-запроса ограничивается одной таблицей. Однако на практике это не слишком удобно, поскольку выполнение SQL-запросов связано с удаленным вызовом функций, а удаленные вызовы крайне медлительны. Изменим предыдущий пример так, чтобы получение информации об альбоме и о его исполнителе осуществлялось в рамках одного SQL-запроса. Первое изменение коснется SQL-выражения, предназначенного для проведения поиска.
class AlbumMapper...
public Album find(Long id) {
return (Album) abstractFind(id);
}
protected String findStatement() {
return "SELECT a.ID, a.title, a.artistID, r.name " +
" from albums a, artists r " +
" WHERE ID = ? and a.artistID = r.ID";
}
Кроме того, воспользуемся другим методом загрузки, который будет загружать данные и об альбоме и об исполнителе.
class AlbumMapper...
protected DomainObject doLoad(Long id, ResultSet rs) throws SQLException {
String title = rs.getString(2);
long artistID = rs.getLong(3);
ArtistMapper artistMapper = MapperRegistry.artist();
Artist artist;
if (artistMapper.isLoaded(artistID))
artist = artistMapper.find(artistID);
else
artist = loadArtist(artistID, rs);
Album result = new Album(id, title, artist);
return result;
}
private Artist loadArtist(long id, ResultSet rs) throws SQLException {
String name = rs.getString(4);
Artist result = new Artist(new Long(id), name);
MapperRegistry.artist().register(result.getID(), result);
return result;
}
При выполнении этой задачи возникает вопрос: куда поместить метод, который отображает результат выполнения SQL-запроса на объект исполнителя? С одной стороны, его лучше поместить в преобразователь исполнителя, так как именно этот класс выполняет загрузку объекта исполнителя. С другой стороны, метод загрузки тесно связан с выполнением SQL-запроса и потому должен оставаться в том классе, в котором определен SQL-запрос. В данном примере я рекомендую второе.
Пример: коллекция ссылок (С#)
Этот случай возникает тогда, когда у объекта есть поле, содержащее коллекцию. В качестве примера рассмотрим ситуацию с командами и игроками. При этом будем исходить из предположения, что игроки не могут быть отображениями зависимых объектов (рис. 12.5).
Рис. 12.5. Команда, состоящая из нескольких игроков
class Team...
public String Name;
public IList Players {
get {return ArrayList.ReadOnly(playersData);}
set {playersData = new ArrayList(value);}
}
public void AddPlayer(Player arg) {
playersData.Add(arg);
}
private IList playersData = new ArrayList();
В базе данных этой связи будет соответствовать запись об игроке, содержащая в себе внешний ключ команды (рис. 12.6).
Рис. 12.6. Структура базы данных для команды, состоящей из нескольких игроков
class TeamMapper...
public Team Find(long id) {
return (Team) AbstractFind(id);
}
class AbstractMapper...
protected DomainObject AbstractFind(long id) {
Assert.True (id != DomainObject.PLACEHOLDER_ID);
DataRow row = FindRow(id);
return (row == null) ? null : Load(row);
}
protected DataRow FindRow(long id) {
String filter = String.Format("id = {0}", id);
DataRow[] results = table.Select(filter);
return (results.Length == 0) ? null : results[0];
}
protected DataTable table {
get {return dsh.Data.Tables[TableName];}
}
public DataSetHolder dsh;
abstract protected String TableName {get;}
class TeamMapper...
protected override String TableName {
get {return "Teams";}
}
Класс DataSetHolder содержит в себе используемое множество данных (объект DataSet), а также объекты DataAdapter, необходимые для передачи обновлений объекта DataSet в базу данных.
class DataSetHolder...
public DataSet Data = new DataSet();
private Hashtable DataAdapters = new Hashtable();
В этом примере мы исходим из предположения, что объект DataSetHolder уже был заполнен результатами выполнения нескольких соответствующих запросов.
Метод поиска вызывает метод загрузки для выполнения фактической загрузки данных в новый объект.
class AbstractMapper...
protected DomainObject Load (DataRow row) {
long id = (int) row ["id"];
if (identityMap[id] != null) return (DomainObject) identityMap[id];
else {
DomainObject result = CreateDomainObject();
result.Id = id;
identityMap.Add(result.Id, result);
doLoad(result,row);
return result;
}
}
abstract protected DomainObject CreateDomainObject();
private IDictionary identityMap = new Hashtable();
abstract protected void doLoad (DomainObject obj, DataRow row);
class TeamMapper...
protected override void doLoad (DomainObject obj, DataRow row) {
Team team = (Team) obj;
team.Name = (String) row["name"];
team.Players = MapperRegistry.Player.FindForTeam(team.Id);
}
Чтобы загрузить список игроков, я реализую в классе PlayerMapper специальный метод поиска.
class PlayerMapper...
public IList FindForTeam(long id) {
String filter = String.Format("teamID = {0}", id);
DataRow[] rows = table.Select(filter);
IList result = new ArrayList();
foreach (DataRow row in rows) {
result.Add(Load (row));
}
return result;
}
Для обновления сведений о команде объект TeamMapper сохраняет ее собственные данные и передает управление объекту PlayerMapper, чтобы тот сохранил обновленные данные в таблице игроков.
class AbstractMapper...
public virtual void Update (DomainObject arg) {
Save (arg, FindRow(arg.Id));
}
abstract protected void Save (DomainObject arg, DataRow row);
class TeamMapper...
protected override void Save (DomainObject obj, DataRow row){
Team team = (Team) obj;
row["name"] = team.Name;
savePlayers(team);
}
private void savePlayers(Team team){
foreach (Player p in team.Players) {
MapperRegistry.Player.LinkTeam(p, team.Id);
}
}
class PlayerMapper...
public void LinkTeam (Player player, long teamID) {
DataRow row = FindRow(player.Id);
row["teamID"] = teamID;
}
В этом примере код обновления довольно прост, потому что ссылка игрока на команду является обязательной. Даже если игрок перейдет из одной команды в другую, не придется выполнять утомительное сравнение для сортировки игроков на добавленных, удаленных и т.п. — вся информация об игроке будет обновлена при обновлении сведений о соответствующих командах. Более сложный случай с необязательными ссылками я выношу на самостоятельное рассмотрение читателей.
