На этой неделе я остановлюсь на вариантах, предлагаемых при отображении иерархий наследования в JPA. JPA предоставляет три способа
сопоставить иерархии наследования Java с таблицами базы данных :
- InheritanceType.SINGLE_TABLE — вся иерархия наследования отображается в одну таблицу. Объект хранится ровно в одной строке в этой таблице, а значение дискриминатора, хранящееся в столбце дискриминатора, указывает тип объекта. Любые поля, не используемые в суперклассе или другой ветви иерархии, устанавливаются в NULL . Это стратегия отображения наследования по умолчанию, используемая JPA.
- InheritanceType.TABLE_PER_CLASS — Каждый конкретный класс сущностей в иерархии сопоставляется с отдельной таблицей. Объект хранится ровно в одной строке в конкретной таблице для его типа. Эта конкретная таблица содержит столбец для всех полей конкретного класса, включая любые унаследованные поля. Это означает, что у братьев и сестер в иерархии наследования будет каждая своя копия полей, которые они наследуют от своего суперкласса. UNION из отдельных таблиц выполняется при запросе на суперкласса.
- InheritanceType.JOINED — Каждый класс в иерархии представлен в виде отдельной таблицы, поэтому дублирование полей не происходит. Объект хранится в нескольких таблицах; по одной строке в каждой из таблиц, составляющих иерархию наследования классов. Отношение is-a между подклассом и его суперклассом представляется как отношение внешнего ключа от «subtable» к «supertable», и сопоставленные таблицы объединяются для загрузки всех полей объекта.
Хорошее сравнение параметров отображения наследования JPA с изображениями, включая описание параметра @MappedSuperclass , можно найти в документации DataNucleus .
Теперь интересный вопрос: какой метод работает лучше при каких обстоятельствах?
SINGLE_TABLE — Одна таблица на иерархию классов
Стратегия SINGLE_TABLE имеет преимущество в простоте. Загрузка сущностей требует запроса только одной таблицы, причем для определения типа сущности используется столбец дискриминатора. Эта простота также помогает при ручной проверке или изменении объектов, хранящихся в базе данных.
Недостатком этой стратегии является то, что одна таблица становится очень большой, когда в иерархии много классов. Кроме того, столбцы, которые сопоставляются с подклассом в иерархии, должны быть обнуляемыми , что особенно раздражает при больших иерархиях наследования. Наконец, изменение какого — либо одного класса в иерархии требует одна таблицы будет изменена , что делает SINGLE_TABLE стратегии подходит только для небольших иерархии наследования.
TABLE_PER_CLASS — таблица для конкретного класса
Стратегия TABLE_PER_CLASS не требует, чтобы столбцы делались обнуляемыми, и в результате получается схема базы данных, которая является относительно простой для понимания. В результате это также легко проверить или изменить вручную.
Недостатком является то, что для полиморфной загрузки сущностей требуется объединение всех сопоставленных таблиц, что может повлиять на производительность. Наконец, дублирование столбца, соответствующего полям суперкласса, приводит к тому, что дизайн базы данных не нормализуется . Это затрудняет выполнение агрегированных (SQL) запросов к дублированным столбцам. Таким образом, эта стратегия лучше всего подходит для широкой, но не глубокой иерархии наследования, в которой поля суперкласса не являются теми, к которым вы хотите обращаться.
СОЕДИНЕННЫЕ — Таблица за класс
Стратегия JOINED дает вам хорошо нормализованную схему базы данных без каких-либо повторяющихся столбцов или ненужных столбцов, допускающих обнуление. Как таковая, она лучше всего подходит для больших иерархий наследования, будь то глубокая или широкая.
Эта стратегия затрудняет проверку или изменение данных вручную. Кроме того, операция JOIN, необходимая для загрузки объектов, может стать проблемой производительности или явным препятствием для размера вашей стратегии наследования. Также обратите внимание, что Hibernate неправильно обрабатывает столбцы дискриминатора при использовании стратегии JOINED .
Кстати, при использовании прокси Hibernate, имейте в виду, что ленивая загрузка класса, сопоставленного с любой из трех вышеуказанных стратегий, всегда возвращает прокси, который является экземпляром суперкласса .
Это все варианты?
Подводя итог, можно сказать, что при выборе из стандартных опций отображения наследования JPA применяются следующие правила:
- Малая иерархия наследования -> SINGLE_TABLE.
- Широкая иерархия наследования -> TABLE_PER_CLASS.
- Глубокая иерархия наследования -> СОЕДИНЕННЫЕ.
Но что если ваша иерархия наследования очень широка или очень глубока? А что если классы в вашей системе часто изменяются? Как мы обнаружили при создании постоянной командной структуры и гибкой CMDB для нашего продукта автоматизации развертывания Java EE Deployit , конкретные классы в нижней части большой иерархии наследования могут часто меняться. Таким образом, эти два вопроса часто получают положительный ответ одновременно. К счастью, есть одно решение обеих проблем!
Используя капли
Первое, что следует отметить, — это то, что наследование является очень большой составляющей несоответствия объектно-реляционного импеданса . И тогда мы должны задать себе вопрос: почему мы даже отображаем все эти часто меняющиеся конкретные классы на таблицы базы данных? Если бы объектные базы данных действительно прорвались, нам было бы лучше хранить эти классы в такой базе данных. Как таковые, реляционные базы данных унаследовали землю, так что об этом не может быть и речи. Возможно также, что для части вашей объектной модели реляционная модель действительно имеет смысл, потому что вы хотите выполнять запросы и иметь базу данных для управления отношениями (внешнего ключа). Но для некоторых частей вы на самом деле заинтересованы только в простом сохранении объектов.
Хорошим примером является «постоянная структура команд», о которой я упоминал выше. Платформа должна хранить общую информацию о каждой команде, такую как ссылка на «план изменений» (вид контекста выполнения), которому она принадлежит, время начала и окончания, вывод журнала и т. Д. Но она также должна хранить объект команды это представляет фактическую работу, которая должна быть сделана (вызов wsadmin или wlst или что-то подобное в нашем случае).
Для первой части лучше всего подходит иерархическая модель. Для второй части подойдет простая сериализация. Итак, сначала мы определим простой интерфейс, который реализуется различными объектами команд в нашей системе:
public interface Command {
void execute();
}
И затем мы создаем объект, который хранит как метаданные (данные, которые мы хотим сохранить в реляционной модели), так и сериализованный объект команды:
@Entity
public class CommandMetaData {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@ManyToOne
private ChangePlan changePlan;
private Date startOfExecution;
private Date endOfExecution;
@Lob
private String log;
@Lob
@Column(name = "COMMAND", updatable = false)
private byte[] serializedCommand;
@Transient
private Command command;
public CommandMetaData(Command details) {
serializedCommand = serializeCommand(details);
}
public Command getCommand() {
if (command != null) {
command = deserializeCommand(serializedCommand);
}
return command;
}
[... rest omitted ...]
}
Поле serializedCommand является байтовый массив , который хранится в виде двоичного объекта в базе данных из-за @Lob аннотации. Имя столбца явно установлено как «КОМАНДА», чтобы предотвратить появление в схеме базы данных имени столбца по умолчанию «SERIALIZEDCOMMAND».
Поле команды помечено как @Transient, чтобы предотвратить его сохранение в базе данных.
Когда создается объект CommandMetaData, передается объект Command. Конструктор сериализует объект команды и сохраняет результаты в поле serializedCommand. После этого команда не может быть изменена (нет метода setCommand ()), поэтому serializedCommand может быть помечен как не обновляемый. Это предотвращает запись этого довольно большого поля большого двоичного объекта в базу данных каждый раз, когда обновляется другое поле CommandMetaData (например, поле журнала).
Каждый раз, когда вызывается метод getCommand, команда десериализуется при необходимости, а затем возвращается. GetCommand может быть помечен как синхронизированный, если этот объект используется в нескольких параллельных потоках.
Некоторые вещи, которые следует отметить об этом подходе:
- Используемый метод сериализации влияет на гибкость этого подхода. Стандартная Java-сериализация проста, но не справляется с изменением классов. XML может быть альтернативой, но у него есть свои проблемы с версиями. Выбор правильного механизма сериализации оставлен в качестве упражнения для читателя.
- Хотя капли были вокруг некоторое время, некоторые базы данных все еще борются с ними. Например, использование больших двоичных объектов с Hibernate и Oracle может быть непростым делом.
- В представленном выше подходе любые изменения, внесенные в объект Command после его сериализации, сохраняться не будут. Умное использование хуков жизненного цикла @PrePersist и @PreUpdate может решить эту проблему.
Этот подход к сохранению полуобъектных баз данных / полуореляционных баз данных хорошо сработал для нас. Мне интересно услышать, пробовали ли другие люди такой же подход и как им это удалось. Или вы подумали о другом решении этих проблем?