Как наследовать свойства сущности и обобщать запросы в 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 используется как база данных в памяти.
  1. Давайте создадим сопоставленный базовый класс сущностей
@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, вам также может понравиться моя статья по теме:



Я надеюсь, что вы узнали что-то новое из этого поста. Спасибо за чтение и удачного кодирования!