В последнее время я занят своим пэт-проектом, и когда я создавал модели сущностей из базы данных, то обратил внимание, что в проекте у меня есть композиция. Поэтому я решил посвятить отношениям между сущностями или классами отдельную статью.
Расскажу о наследовании, агрегации, композиции и ассоциации. Сначала пробегусь по этим отношениям со случайными примерами (без JPA и прочих наворотов, на простой джаве), а в конце статьи покажу пример композиций из моего проекта (уже с Jakarta EE).
Наследование
Одно из базовых отношений в объектно-ориентированном программировании. Тут даже есть специальное слово extends. чтобы показывать такое отношение между классами. В английском мы бы назвали его «is a», то есть «наследник» это есть наследуемый объект, просто с расширенным функционалом.
Один из лучших примеров, это иерархия биологических видов. Отряд можно считать потомком класса, а конкретные виды уже потомки отряда.
В моей статье я конечно же выберу более музыкальный пример. Песня и ремикс. Ремикс имеет те же свойства, что и сама песня (то же название, те же исполнители), но она получает дополнительную характеристику: новый диджей или продюсер, который создал ремикс.

public class Song {
private String title;
private String artists;
}
public class Remix extends Song {
private String producer;
}
Композиция
Композиция предполагает отношения «has a», то есть один объект выступает как контейнер, а другой — как его содержимое. Но фишка в том, что объекты не могут существовать друг без друга, поэтому такая ситуация и называется композицией.
Хороший пример, это здание и комнаты. Без комнат здания быть не может — в нём должна быть хотя бы одна комната. С другой стороны, комнаты не могут существовать просто так, в воздухе, без здания. Или группа и студенты. Студенты не могут быть не закреплены ни за одной группой, а группа без студентов не имеет смысла — кто-то же в ней должен учиться.
Мой музыкальный пример — это издание и альбом. Альбом выходит на конкретных носителях (или в цифровом варианте), и каждый вариант имеет свой каталожный номер и выпускается на конкретном лейбле. Без изданий альбома не будет, но и без альбома не будет изданий — будут просто пустые пластинки.

В коде такое отношение нужно отразить таким образом, чтобы Издание не могло инициализироваться вне альбома. Чтобы никто ненароком не создал Издание, не относящееся ни к какому альбому. И в объекте альбома обязательно должен быть список изданий (в других примерах это не обязательно список, достаточно поместить хотя бы одиночный объект в контейнер).
public class Album {
private String title;
private String artist;
private List<Edition> editions;
{
editions = new ArrayList<>();
}
public addEdition () {
editions.add(new Edition());
}
class Edition {
private String catNumber;
private String lable;
}
}
Получаем что-то вроде этого кода. И сразу оговорюсь, что я не расписываю все конструкторы, а просто накидываю код, для понимания происходящего. Вместо такой конструкции можно использовать анонимные классы, а не вложенные.
Да и на самом-то деле, можно вообще оформить их двумя отдельными классами, но тогда логику их совместного существования нужно поддерживать в коде, чтобы избежать неприятных последствий. Такой пример я показываю ниже в примере из своего пет-проекта.
Агрегация
Агрегация похожа на композицию, с разницей, что объекты могут существовать друг без друга и по логике и в создаваемой модели.
Например, автомобиль и его детали. Детали можно вытащить, а автомобиль всё равно останется.
Мой музыкальный пример это исполнители и песни в его дискографии. У исполнителя можно отобрать права на песню, её может исполнить кто-то другой, песня вообще может существовать лишь в нотном варианте без официальных исполнителей. Да и исполнитель может существовать без своих собственных песен и петь каверы.

В этом случае класс Исполнителя всё ещё будет содержать список его песен, но в коде правильно будет сделать Песни отдельным, не вложенным классом. Наполнять список песен нужно уже самостоятельно.
public class Artist {
private String name;
private List<Song> songs;
}
public class Song {
private String author;
}
Ассоциация
Ассоциация не предполагает никаких близких отношений. Один объект не является другим, и не содержит его в себе, но они ассоциируются друг с другом. То есть один объект знает о существовании другого.
Например, учитель и предмет. Или спектакль и театр. Или организация и номер телефона. Они из одной предметной области, но не более.
Мой пример это альбом и студия звукозаписи. В буклете обычно указывают, где записан альбом, и для некоторых слушателей это важная информация. Поэтому в класс Альбом можно поместить информацию об этом. Но Студия и сам Альбом существуют вполне независимо друг от друга.

public class Album {
private String title;
private String artist;
private Studio studio;
}
public class Studio {
private String address;
}
Композиция в JPA (пример из моего проекта)
Поверхностно я пробежался по разным отношениям, и завершу статью примером из моего проекта с JPA, то есть с базой данных.
В моём проекте есть Чарт и Позиция. Очевидно, что Позиция не может существовать просто в вакууме и должна быть связана с чартом. Но и Чарт без позиций не имеет смысла. Получается композиция.
Фишка с вложенными и анонимными классами здесь не прокатит, так как для работы приложения с базой данных (Hibernate и MySQL) необходимо иметь самостоятельный класс в отдельном классе для каждой таблицы в базе. Поэтому ограничимся списком Позиций в Чарте и Чартом в Позиции (чтобы отразить внешний ключ в базе).
@Entity
@Table(name="chart")
public class Chart {
@Id
@Column(name="idChart")
private int id;
@Column(name="Date")
private String date;
@OneToMany(mappedBy = "pk.chart", cascade = CascadeType.ALL)
private List<Position> positions;
//геттеры, сеттеры и конструкторы должны быть здесь
}
Видим список позиций в чарте. Не забываем указывать, с каким полем в Позиции он связан в аннотации по свойству mappedBy. В моём случае это chart в pk — поле, которое содержит составной ключ.
Теперь взглянем на классы Позиции и класс для составного ключа Позиций (о составном ключе я писал в прошлой статье). Здесь уже никаких mappedBy, только JoinColumn, в которой указываем имя соответствующей колонки в таблице position в БД (у меня в таблице она называется idChart) .
Ещё раз повторю этот момент, потому что мне он казался нелогичным. Если в таблице в базе есть внешний ключ, то мы используем JoinColumn и указываем там имя этой колонки. Без этого никак. А вот в контейнере нашей композиции можем по желанию создать список с mappedBy, где указываем не название колонки (ведь в базе её просто нет), а название нужного поля в классе.
@Entity
@Table(name="position")
public class Position {
@EmbeddedId
private Position_PK pk;
@Column(name="Position")
private int position;
@Column(name="LastWeek")
private Integer lastWeek;
//здесь должны быть конструкторы, геттеры и сеттеры
}
@Embeddable
public class Position_PK implements Serializable {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "idChart")
private Chart chart;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "idSong")
private Song song;
}
В этом случае видим отношения @ManyToOne (и @OneToMany в обратную сторону) — много позиций в одном чарте.
Но отношения могли бы быть и @OneToOne (естественно, тогда был бы не нужен список) или @ManyToMany, но он уже оформляется с помощью промежуточного класса (собственно, как и в базе нужна промежуточная таблица — а джава уже просто будет копировать эту структуру).
На этом всё. Я возвращаюсь к написанию проекта, и в следующих статьях продолжу с менее общими темами. Расскажу про метод POST, и как с jsp страницы передавать информацию в базу, а не просто из неё её читать.
1 Comment