- •Оглавление
- •Предисловие
- •Введение
- •Часть 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)
- •Список типовых решений
- •Шпаргалка
- •Как управлять сложным потоком функций приложения?
- •Как взаимодействовать с базой данных?
- •Как избежать загрузки в оперативную память всего содержимого базы данных?
- •Как сохранить структуры наследования в реляционной базе данных?
Наследование с одной таблицей (Single Table Inheritance)
Представляет иерархию наследования классов в виде одной таблицы, столбцы которой соответствуют всем полям классов, входящих в иерархию
Реляционные базы данных не поддерживают наследование. Выполняя отображение объектной модели на базу данных, необходимо найти способ, позволяющий отобразить структуру наследования. Разумеется, крайне важно минимизировать количество соединений, которое стремительно возрастает при попытке отображения структуры наследования на разные таблицы. На помощь приходит типовое решение наследование с одной таблицей, отображающее все поля всех классов структуры наследования на столбцы одной и той же таблицы.
Принцип действия
Итак, в данном случае структура наследования отображается на одну таблицу, которая содержит в себе все данные всех классов, входящих в иерархию наследования. Каждому классу (а точнее, его экземпляру) соответствует одна строка таблицы; при этом поля таблицы, которых нет в данном классе, остаются пустыми. Основное поведение объектов, выполняющих отображение, соответствует общей схеме преобразователей наследования (Inheritance Mappers).
Выполняя загрузку объекта в память, необходимо знать, в экземпляр какого класса следует поместить загружаемые данные. Для этого к таблице добавляется специальное поле, указывающее на то, экземпляр какого класса должен быть создан для загрузки данного объекта. Это может быть имя класса или какое-нибудь кодовое поле. Для отображения кодового поля на имя соответствующего класса необходим специальный код, который должен быть расширен при добавлении в иерархию нового класса. В свою очередь, указанное в таблице явное имя класса можно использовать непосредственно для создания экземпляра этого класса. Следует отметить, что явное имя класса занимает больше места и является более сложным для обработки при непосредственном использовании таблиц базы данных. Кроме того, оно теснее привязывает структуру классов к схеме базы данных.
Перед загрузкой данных необходимо считать код типа класса, чтобы узнать, экземпляр какого производного класса должен быть создан для загрузки объекта. При сохранении объекта в базе данных запись кода типа класса выполняется суперклассом.
Назначение
Типовое решение наследование с одной таблицей является одним из вариантов отображения иерархии наследования на реляционную базу данных. В число других возможных вариантов входят наследование с таблицами для каждого класса (Class Table Inheritance) и наследование с таблицами для каждого конкретного класса (Concrete Table Inheritance).
Наследование с одной таблицей имеет ряд преимуществ.
В структуру базы данных добавляется только одна таблица.
Для извлечения данных не нужно выполнять соединение таблиц.
Перемещение полей в производный класс или суперкласс не требует внесения изменений в структуру базы данных.
Несмотря на это, у данного типового решения есть и слабые стороны.
Не все поля соответствуют содержимому каждого конкретного объекта, что может приводить в замешательство людей, работающих только с таблицами.
Некоторые столбцы используются только одним-двумя производными классами, что приводит к бессмысленной трате свободного места. Критичность данной проблемы зависит от характеристик конкретных данных, а также от того, насколько хорошо сжимаются пустые поля. Например, в базах данных Oracle применяется высокая степень сжатия свободного пространства, особенно если "необязательные" столбцы выносятся в правую часть таблицы. Впрочем, у каждой базы данных есть свои приемы на этот счет.
Полученная таблица может оказаться слишком большой, с множеством индексов и частыми блокировками, что будет оказывать негативное влияние на производительность базы данных. Во избежание этой проблемы можно создать отдельные таблицы индексов, которые будут содержать ключи строк, имеющих определенное свойство, или же копии подмножеств полей, имеющих отношение к индексам.
Все имена столбцов таблицы принадлежат единому пространству имен, поэтому необходимо следить за тем, чтобы у полей разных классов не было одинаковых имен. Для облегчения работы рекомендую называть поля составными именами с указанием имени содержащего их класса в качестве префикса или суффикса.
Запомните: вы вовсе не обязаны использовать единственную форму отображения для всей иерархии наследования. Вполне естественно отобразить 5-10 классов с похожей структурой в общую таблицу, в то время как классы со множеством специфичных данных будут отображаться с использованием наследования с таблицами для каждого конкретного класса.
Пример: общая таблица игроков (С#)
Как и другие примеры обработки иерархий наследования, данный пример основан на использовании преобразователей наследования и объектной модели, изображенной на рис. 12.8. Каждый преобразователь должен ссылаться на таблицу DataTable объекта ADO.NET DataSet. Эта ссылка может быть реализована в общем виде в суперклассе Mapper. Значением свойства Data класса Gateway является объект DataSet, содержимое которого может быть загружено в результате выполнения запроса.
class Mapper...
protected DataTable table {
get {return Gateway.Data.Tables[TableName];}
}
protected Gateway Gateway;
abstract protected String TableName {get;}
Поскольку таблица всего одна, ее можно определить в абстрактном классе АbstractPlayerMapper.
class AbstractPlayerMapper...
protected override String TableName {
get {return "Players";}
}
Рис. 12.8. Универсальная схема классов для преобразователей наследования
Каждому классу нужно поставить в соответствие код типа класса, чтобы преобразователь знал, с каким игроком он имеет дело. Код типа определяется в суперклассе и реализуется в производных классах.
class AbstractPlayerMapper...
abstract public String TypeCode {get;}
class CricketerMapper...
public const String TYPE_CODE = "C";
public override String TypeCode {
get {return TYPE_CODE;}
}
Класс PlayerMapper содержит по одному полю на каждый из трех конкретных классов преобразователей (и соответственно на каждый из трех типов игроков).
class PlayerMapper...
private BowlerMapper bmapper;
private CricketerMapper cmapper;
private FootballerMapper fmapper;
public PlayerMapper (Gateway gateway) : base (gateway) {
bmapper = new BowlerMapper(Gateway);
cmapper = new CricketerMapper(Gateway);
fmapper = new FootballerMapper(Gateway);
}
Загрузка объекта из базы данных
Каждый конкретный класс преобразователя содержит метод поиска для извлечения объекта из базы данных.
class CricketerMapper...
public Cricketer Find(long id) {
return (Cricketer) AbstractFind(id);
}
Для выполнения поиска данный метод вызывает универсальный метод суперкласса.
class Mapper...
protected DomainObject AbstractFind(long id) {
DataRow row = FindRow(id);
return (row == null) ? null : Find(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];
}
public DomainObject Find (DataRow row) {
DomainObject result = CreateDomainObject();
Load(result, row);
return result;
}
abstract protected DomainObject CreateDomainObject();
class CricketerMapper...
protected override DomainObject CreateDomainObject() {
return new Cricketer();
}
Для загрузки данных в новый объект я применяю группу методов загрузки — по одному в каждом классе иерархии.
class CricketerMapper...
protected override void Load(DomainObject obj, DataRow row) {
base.Load(obj,row);
Cricketer cricketer = (Cricketer) obj;
cricketer.battingAverage = (double)row["battingAverage"];
}
class AbstractPlayerMapper...
protected override void Load(DomainObject obj, DataRow row) {
base.Load(obj, row);
Player player = (Player) obj;
player.name = (String)row["name"];
}
class Mapper...
protected virtual void Load(DomainObject obj, DataRow row) {
obj.Id = (int) row ["id"];
}
Вместо этого я могу загрузить сведения об игроке с помощью преобразователя PlayerMapper. Он считывает данные и использует код типа класса, чтобы определить, какой конкретный преобразователь нужно использовать в данном случае.
class PlayerMapper...
public Player Find (long key) {
DataRow row = FindRow(key);
if (row == null) return null;
else {
String typecode = (String) row["type"];
switch (typecode){
case BowlerMapper.TYPE_CODE:
return (Player) bmapper.Find(row);
case CricketerMapper.TYPE_CODE:
return (Player) cmapper.Find(row);
case FootballerMapper.TYPE_CODE:
return (Player) fmapper.Find(row);
default:
throw new Exception("unknown type");
}
}
}
Обновление объекта
Суть операции обновления одинакова для всех классов, поэтому ее можно определить в суперклассе Mapper.
class Mapper...
public virtual void Update (DomainObject arg) {
Save (arg, FindRow(arg.Id));
}
Метод сохранения аналогичен методу загрузки, т.е. определен в каждом производном классе для сохранения соответствующих данных.
class CricketerMapper...
protected override void Save(DomainObject obj, DataRow row) {
base.Save(obj, row);
Cricketer cricketer = (Cricketer) obj;
row["battingAverage"] = cricketer.battingAverage;
}
class AbstractPlayerMapper...
protected override void Save(DomainObject obj, DataRow row) {
Player player = (Player) obj;
row["name"] = player.name;
row["type"] = TypeCode;
}
Преобразователь playerMapper обращается к нужному конкретному преобразователю.
class PlayerMapper...
public override void Update (DomainObject obj) {
MapperFor(obj).Update(obj);
}
private Mapper MapperFor(DomainObject obj) {
if (obj is Footballer)
return fmapper;
if (obj is Bowler)
return bmapper;
if (obj is Cricketer)
return cmapper;
throw new Exception("No mapper available");
}
Вставка объекта
Выполнение вставки аналогично обновлению; единственная существенная разница состоит в том, что перед сохранением данных в таблице нужно создать новую строку.
class Mapper...
public virtual long Insert (DomainObject arg) {
DataRow row = table.NewRow();
arg.Id = GetNextID();
row["id"] = arg.Id;
Save (arg, row);
table.Rows.Add(row);
return arg.Id;
}
class PlayerMapper...
public override long Insert (DomainObject obj) {
return MapperFor(obj).Insert(obj);
}
Удаление объекта
Удалить объект очень просто. Операции удаления определены на абстрактном уровне, а также в классе-оболочке PlayerMapper.
class Mapper...
public virtual void Delete(DomainObject obj) {
DataRow row = FindRow(obj.Id);
row.Delete();
}
class PlayerMapper...
public override void Delete (DomainObject obj) {
MapperFor(obj).Delete(obj);
}
