JPA и связи между объектами

database-relationships-091201140018-phpapp01-thumbnail-4Классы в Java могут не только наследоваться друг от друга, но и включать в себя другие классы или даже коллекции классов в качестве полей. Мы уже знаем, что в столбцах таблиц, за некоторыми исключениями, нельзя хранить сложные составные типы и коллекции таких типов, что не позволяет сохранять весь подобный объект в одну таблицу. Зато можно сохранять каждый класс в свою собственную таблицу и сохранять связи между ними.

@OneToOne

Предположим что мы пишем приложение для какой-нибудь государственной организации, занимающейся учётом граждан. Мы хотим в этом приложении хранить данные о гражданах и о выданных им паспортах. Очевидно, что у нас будет сущность «гражданин», описывающая конкретного человека и сущность «паспорт», описывающая конкретный паспорт. Мы так же знаем, что каждый гражданин имеет паспорт и при этом каждый паспорт принадлежит какому-либо гражданину. Такая связь называется один к одному.

Оба класса наследованы от @MappedSuperclass, который определяет суррогатный первичный ключ.

В каждой связи есть понятие владелец и владеемый. В примере выше класс Person владеет классом Passport. Для связи один к одному в обоих классах к полю добавляется аннотация @OneToOne, параметр optional которой говорит , является ли значение в этом поле обязательным или нет.

Со стороны владельца к аннотации @OneToOne добавляется так же параметр cascade,  который говорит JPA, что делать с владеемыми объектами при операциях над владельцем.

Про каскадирование стоит рассказать подробнее. Предположим, что никакого каскадирования в JPA нет и никогда не было. В это случае, если мы хотим удалить гражданина из нашей базы данных, мы должны помнить, что у него есть паспорт и должны вначале вручную удалить паспорт, затем только удалить гражданина. Если мы ничего не сделаем с паспортом, в базе останется паспорт, указывающий на несуществующего гражданина, что не хорошо. На самом деле, конечно же, в базе будет foreign key constraint, которые просто не даст сделать такое действие, что вообщем-то, не сильно лучше для конечного пользователя.

Каскадирование позволяет сказать JPA «сделай с владеемыми объектами класса тоже самое, что ты делаешь с владельцем». То есть, когда мы удаляем гражданина из базы, JPA самостоятельно увидит, что гражданин владеет паспорт и удалит вначале паспорт, потом гражданина.

Другой случай использования каскадирования характерен именно для связи один к одному. Если мы без каскадирования создадим паспорт и гражданина, мы не сможем их по раздельности сохранить, потому что паспорт будет ссылаться на гражданина, которого ещё нет в базе данных, а гражданин будет ссылаться на паспорт, которого тоже ещё нет в базе данных. Каскадирование позволяет, в данном случае, обойти эту проблему.

Размеется, CascadeType.ALL это не единственный возможный тип каскадирования, но их отличиям я посвящу отдельную статью.

Итак, кроме настроек каскадирования владелец связи один к одному добавляет к полю кроме аннотации @OneToOne  ещё и аннотацию @JoinColumn, которая задаёт имя столбца, в котором будет храниться ссылка на владеемый объект. Физически это будет столбец с заданным именем, таким же типом, как у первичного ключа владеемого объекта и содержаться в нём будут значения первичных ключей владеемых объектов.

Со стороны паспорта, то есть владеемого объекта, такой столбец не требуется, а требуется в аннотации @OneToOne  задать параметр mappedBy, который указывает, какое поле в объекте владельце (то есть классе Person) соответствует владеемому объекту (то есть passport).

@OneToMany

Кроме паспорта мы хотим хранить для граждан так же и место проживания. Очевидно, что один гражданин имеет один основной адрес проживания, но при этом по одному адресу может проживать несколько человек. Граждане и адреса образуют связь один ко многим и это самый распространённый тип связи между классами.

Владельцем в данном случае опять будет класс Person, который аннотирует поле primaryAddress аннотациями  @ManyToOne и @JoinColumn. Параметры этих аннотаций несут ту же смысловую нагрузку, что и у связи один к одному.

У владеемого объекта в этот раз всё по другому. Жильцы, которых по одному адресу может быть несколько, представлены коллекцией, которая аннотирована @OneToMany. Параметр mappedBy так же указывает на поле с владеемым в классе владельце. А вот параметр fetch = FetchType.EAGER говорит, что при загрузке владеемого объекта необходимо сразу загрузить и коллекцию владельцев.

Стратегии загрузки (fetch) бывает две: EAGER и LAZY. В первом случае объекты коллекции сразу загружаются в память, во втором случае только при обращении к ним. Оба подхода имеют достоинства и недостатки. В случае FetchType.EAGER в памяти будут находиться полностью загруженные и готовые к употреблению объекты. При этом они будут эту самую память занимать и если вам нужен только один объект из сотен (тысяч), то занимать они её будут просто так. Кроме того, при загрузке какого-нибудь корневого объекта, который связан со всеми остальными объектами и коллекциями, можно случайно попытаться загрузить в память и всю базу 🙂

С другой стороны, FetchType.LAZY загружает объекты только по мере обращения, но при этом требует, чтобы соединение с базой (или транзакция) сохранялись. Если быть точно, требует, чтобы объект был attached, но и про это я расскажу позднее. Поэтому для работы с lazy объектами тратится больше ресурсов на поддержку соединений.

@ManyToMany

И наконец, мы бы хотели хранить сведения о местах работы. Один гражданин может работать в нескольких компаниях, а в каждой компании работает несколько человек. Это — связь многие ко многим.

Связь многие ко многим с обоих сторон представлена коллекцией объектов. Так как напрямую в реляционных базах данных такая связь не поддерживается, JPA реализует её с помощью промежуточной таблицы, которая описывается аннотацией @JoinTable у объекта владельца. Параметр name задаёт имя промежуточной таблицы, joinColumns — имя столбца, связывающего с классом владельцем, inverseJoinColumns — имя столбца, связывающего с владеемым классом.

Во владеемом классе аннотацией @ManyToMany отмечаем поле с коллекцией объектов класса владельца. Параметр mappedBy опять указывает на поле с коллекцией владеемых объектов в классе владельце

Использовать классы со связями довольно просто — вначале наполняем их данными, как обычные классы без всякого JPA, а потом, благодаря каскадированию, сохраняем объект класса владельца и JPA делает всё остальное:

Опасность, которая ожидает нас при использовании связей в классах, это циклические связи и вытекающая из них рекурсия. Например метод toString() в классе Passport реализован без вызова toString() у класса Person

Потому что, если бы он просто вызывал toString() у Person, то тот, в свою очередь, вызывал бы toString() у своего поля passport и всё закончилось бы бесконечной рекурсией и StackOverFlowException. Пример с toString() разумеется весьма банален, но в реальных приложениях вляпаться в такую рекурсию при обходе иерархии связей к сожалению не просто, а очень просто.

Код примера доступен на github.