Наследование, агрегация, ассоциация и композиция в Java и JPA

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

Расскажу о наследовании, агрегации, композиции и ассоциации. Сначала пробегусь по этим отношениям со случайными примерами (без 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

Оставить комментарий