Как наследовать свойства сущности и обобщать запросы в Spring Boot JPA
Работа со сложными иерархиями сущностей может быть сложной задачей, особенно когда вам приходится многократно повторять одни и те же свойства и запросы в нескольких конкретных реализациях. Чтобы решить эту проблему, Spring JPA предоставляет @MappedSuperclass
, который позволяет наследовать базовые свойства и запросы.
Например, если все ваши объекты домена требуют общих свойств, таких как дата создания, дата изменения и идентификатор, вы можете определить их в базовом классе, таком как BaseDomainEntity
, и наследовать от него другие ваши объекты домена. Конкретные объекты не будут иметь никакой связи с базовым объектом.
Мы также можем воспользоваться аннотацией @NoRepositoryBean
, которая позволяет создавать общие методы в интерфейсе базового репозитория без создания его экземпляра как Spring Bean.
Эти методы помогают уменьшить дублирование кода.
В этом руководстве вы узнаете, как использовать @MappedSuperclass
и @NoRepositoryBean
для повышения удобства обслуживания вашего приложения Spring Boot.
Давайте погрузимся в это!
Демонстрационный проект
В этом проекте мы будем использовать вызовы REST для вызова серверной службы, извлекающей данные из базы данных. В конце проверим сгенерированные запросы.
Я использую Maven в качестве инструмента сборки для этой демонстрации. Нам нужны следующие зависимости в pom.xml
:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.0.6</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies>
spring-boot-starter-data-jpa
позволяет использовать запросы JPA.spring-boot-starter-web
включает веб-возможности, такие как использование REST.h2
используется как база данных в памяти.
- Давайте создадим сопоставленный базовый класс сущностей
@MappedSuperclass public class MappedBaseEntity { @Id private String id; private String status; private String type; }
Когда класс снабжен аннотацией @MappedSuperclass
, он не сопоставляется с таблицей сам по себе, но его подклассы наследуют его свойства и сопоставления. Подклассы могут дополнительно настраивать сопоставления и добавлять дополнительные свойства.
2. Создайте конкретные объекты, расширяющие базовый класс
@Entity @Table(name = "A") public class ConcreteEntityA extends MappedBaseEntity { String date; } @Entity @Table(name = "B") public class ConcreteEntityB extends MappedBaseEntity { int number; }
Обратите внимание, что эти классы автоматически наследуют поля, определенные в суперклассе. Я добавил два поля, date
и number
, специфичные только для этих классов.
3. Создайте общий репозиторий
@NoRepositoryBean public interface CommonRepository<T1, T2> extends JpaRepository<T1, T2> { List<T1> findByType(String type); }
Когда интерфейс репозитория помечен @NoRepositoryBean
, он служит шаблоном для конкретных интерфейсов репозитория, которые его расширяют. Здесь мы можем определить общие запросы.
Метод findByType
будет доступен во всех репозиториях, расширяющих класс CommonRepository
.
4. Создайте конкретные репозитории, расширяющие общий репозиторий.
public interface ConcreteARepository extends CommonRepository<ConcreteEntityA, String> { } public interface ConcreteBRepository extends CommonRepository<ConcreteEntityB, String> { ConcreteEntityB findByNumber(int number); }
Как объяснялось выше, репозитории будут иметь доступ к методу findByType
.
У ConcreteBRepository
есть дополнительный запрос, а именно findByNumber
.
Вы заметили, что я не использовал аннотацию @Query
? Это связано с тем, что Spring Data JPA использует механизм вывода запроса для идентификации запроса по имени метода.
5. Создайте демо-сервис, который вызывает запросы
@Service public class DemoService { private final ConcreteARepository concreteARepository; private final ConcreteBRepository concreteBRepository; public DemoService(ConcreteARepository concreteARepository, ConcreteBRepository concreteBRepository) { this.concreteARepository = concreteARepository; this.concreteBRepository = concreteBRepository; } public List<ConcreteEntityA> getA() { return concreteARepository.findByType("type"); } public List<ConcreteEntityB> getB() { return concreteBRepository.findByType("type"); } public ConcreteEntityB getBFromNumber(int number) { return concreteBRepository.findByNumber(number); } }
6. Создайте контроллер REST, который вызывает службу для возврата данных из базы данных.
@RestController public class DemoController { private final DemoService demoService; public DemoController(DemoService demoService) { this.demoService = demoService; } @GetMapping(path = "/a") public List<ConcreteEntityA> getAAttributes() { return demoService.getA(); } @GetMapping(path = "/b") public List<ConcreteEntityB> getBAttributes() { return demoService.getB(); } @GetMapping(path = "/b/number") public ConcreteEntityB getBNumber(@RequestParam int number) { return demoService.getBFromNumber(number); } }
Нам не нужно заполнять базу данными. В этой области нас интересуют только сгенерированные запросы.
7. Включите вывод запросов JPA в application.yml
spring: jpa: show-sql: true
Это запишет сгенерированный SQL-запрос в консоль.
Протестируйте приложение
1. Запустите приложение и вызовите конечные точки, используя curl
в своем терминале.
GET http://localhost:8080/a GET http://localhost:8080/b GET http://localhost:8080/b/number?number=1
2. Проверьте журналы на сгенерированные запросы
Hibernate: select concreteen0_.id as id1_0_, concreteen0_.status as status2_0_, concreteen0_.type as type3_0_, concreteen0_.date as date4_0_ from a concreteen0_ where concreteen0_.type=? Hibernate: select concreteen0_.id as id1@MappedSuperclass
, concreteen0_.status as status2@MappedSuperclass
, concreteen0_.type as type3@MappedSuperclass
, concreteen0_.number as number4@MappedSuperclass
from b concreteen0_ where concreteen0_.type=? Hibernate: select concreteen0_.id as id1@MappedSuperclass
, concreteen0_.status as status2@MappedSuperclass
, concreteen0_.type as type3@MappedSuperclass
, concreteen0_.number as number4@MappedSuperclass
from b concreteen0_ where concreteen0_.number=?
Мы видим базовые поля в запросе. Он также включает в себя определенные поля конкретных сущностей — date4_0_
и number4_1_
.
Он работает так, как ожидалось!
Заключение
В этом руководстве вы узнали, как использовать аннотации MappedSuperclass
и NoRepositoryBean
для наследования общих свойств и поведения. Таким образом, кодовая база становится более удобной для сопровождения, а дублирование сокращается.
Полный исходный код этой демонстрации можно найти в моем репозитории GitHub.
Если вас интересуют другие темы JPA, вам также может понравиться моя статья по теме:
Я надеюсь, что вы узнали что-то новое из этого поста. Спасибо за чтение и удачного кодирования!