Если классы-значения, встраиваемые в классы-сущности, организованы в виде иерархии, то при их отображении в БД необходимо учитывать разветвлённость этой иерархии. Если иерархия никак не ветвится, то есть у каждого предка в цепочке наследования есть только один потомок, то ничего особого предпринимать не нужно, достаточно делать стандартную разметку аннотацией @Embeddable.
Если у какого-то из встраиваемых классов-предков более одного потомка, то есть иерархия наследования начинает ветвиться, то может понадобиться более тонкая разметка классов аннотациями.
Подготовка
Создадим базовое веб-приложения на связке Spring Boot 3 + Hibernate + PostgreSQL
Убедитесь, что файле /src/main/resources/application.properties есть следующая строка, позволяющая Hibernate’у автоматически создавать (и обновлять) схему БД при запуске приложения на основании аннотаций в классах предметной области:
spring.jpa.hibernate.ddl-auto=update
Код
Создадим абстрактный класс-предок, от которого будут наследоваться конкретные классы:
1 2 3 4 5 6 7 |
@MappedSuperclass public class AbstractName { private String firstName; private String lastName; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Мы помечаем класс-предок аннотацией @MappedSuperclass, чтобы указать Hibernate’у, что отображаться в таблицы по тем или иным правилам будут именно наследники этого класса, а не он сам. Все правила отображения будут заданы в наследниках.
Создадим классы-наследники, которые будут встраиваться в сущности, и поля которых будут отображаться в таблицах сущностей (каждый в своём файле):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Embeddable @AttributeOverride( name = "firstName", column = @Column(name = "FIRST_NAME_IN_RUSSIAN") ) @AttributeOverride( name = "lastName", column = @Column(name = "LAST_NAME_IN_RUSSIAN") ) public class RussianTranslation extends AbstractName { private String nameVariation; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Embeddable @AttributeOverride( name = "firstName", column = @Column(name = "FIRST_NAME_IN_ENGLISH") ) @AttributeOverride( name = "lastName", column = @Column(name = "LAST_NAME_IN_ENGLISH") ) public class EnglishTranslation extends AbstractName { private String postfix; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Классы RussianTranslation и EnglishTranslation наследуют объявленный выше класс AbstractName и помечаются аннотацией @Embeddable, что указывает Hibernate’у, что у этих классов не будет соответствующих таблиц, а данные этих классов будут храниться в таблицах, созданных для классов-сущностей, которые содержат поля типа RussianTranslation или EnglishTranslation.
Аннотации @Embeddable было бы достаточно, если бы мы были уверены, что не окажется ни одного класса-сущности, который бы содержал одновременно и поле RussianTranslation, и поле EnglishTranslation. Но именно такой класс-сущность мы и создадим далее. И в принципе стоит закладываться на то, что по мере развития модели предметной области могут появляться и одновременно использоваться всевозможные наследники тех или иных классов.
Поэтому в нашем случае классы RussianTranslation и EnglishTranslation дополнительно размечены аннотациями @AttributeOverride, в которых переопределяются имена колонок, создаваемых для отображения данных полей родительского класса. Если этого не сделать, приложение не сможет подняться.
Создадим класс-сущность, в который и будут встраиваться классы-значения RussianTranslation и EnglishTranslation:
1 2 3 4 5 6 7 8 9 10 11 |
@Entity public class Person { @Id @GeneratedValue private Long id; private RussianTranslation nameRussian; private EnglishTranslation nameEnglish; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Создадим репозиторий к этому классу:
1 2 3 |
@Repository public interface PersonRepository extends JpaRepository<Person, Long> { } |
Напишем тест, который продемонстрирует работу нашего кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@SpringBootTest class SpringHibernatePostgresqlApplicationTests { @Autowired PersonRepository personRepository; @Test void embeddedHierarchyMappingTest() throws Exception { RussianTranslation russianTranslation = new RussianTranslation("Карл", "Виндзор", "Чарльз"); EnglishTranslation englishTranslation = new EnglishTranslation("Charles", "Windsor", "III"); Person charlesIII = new Person(russianTranslation, englishTranslation); personRepository.save(charlesIII); Person charlesInDb = personRepository.findById(charlesIII.getId()).get(); assertEquals(charlesIII, charlesInDb); } } |
Тест создаёт сущность Person, предварительно снабдив её данными встраиваемых классов. Затем сохраняет данные в БД и тут же извлекает их. Убеждается, что сохранявшиеся данные идентичны извлечённым.
Посмотрим на схему БД, которую создал Hibernate на основе наших аннотаций:
Как мы и ожидали, у нас одна сущность — Person и в БД создана только одна таблица. Данные обоих встраиваемых классов RussianTranslation и EnglishTranslation хранятся в одной строке этой таблицы. И у RussianTranslation, и у EnglishTranslation есть поля firstName и lastName, унаследованные от предка AbstractName. Но внутри этой таблицы никаких конфликтов имён нет (и вообще всё прекрасно запустилось и работает) благодаря тому, что мы переопределили имена колонок, создаваемых для этих двух полей у каждого из потомков:
- firstName и lastName класса RussianTranslation называются FIRST_NAME_IN_RUSSIAN и LAST_NAME_IN_RUSSIAN.
- firstName и lastName класса EnglishTranslation соответственно называются FIRST_NAME_IN_ENGLISH и LAST_NAME_IN_ENGLISH.
Если бы класс-сущность Person не содержал сразу оба поля RussianTranslation и EnglishTranslation, то переопределение имён было бы необязательным. Но поскольку проекты обычно постоянно развиваются и рефакторятся, то довольно разумно заранее закладывать такой сценарий и переопределять поля предков у встраиваемых классов-потомков.