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

Отображение внешних ключей (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;

}

В этом примере код обновления довольно прост, потому что ссылка игрока на коман­ду является обязательной. Даже если игрок перейдет из одной команды в другую, не при­дется выполнять утомительное сравнение для сортировки игроков на добавленных, уда­ленных и т.п. — вся информация об игроке будет обновлена при обновлении сведений о соответствующих командах. Более сложный случай с необязательными ссылками я вы­ношу на самостоятельное рассмотрение читателей.

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