Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Shablony_korporativnykh_prilozheniy_Fauler_M.docx
Скачиваний:
1
Добавлен:
01.07.2025
Размер:
3.82 Mб
Скачать

Наследование с одной таблицей (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);

}

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]