JpaRepository Query to DTO problem

Учебный проект. Сервис на Spring web + Lombok + Spring data jpa + Postgresql driver + Liquibase migrations. Есть 2 сущности:

Client:

@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "clients")
public class Client {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "card_number")
    private String cardNumber;

    @Column(name = "discount_points")
    private int discountPoints;
}

Check:

@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "order_checks")
public class Check {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "client_id")
    private Long clientId;

    @Column(name = "amount")
    private BigDecimal amount = BigDecimal.valueOf(0);
}

Безхотростный репозиторий для клиента. Сервисы для клиента и чеков типа:

public interface ClientService {

    void addClient(Client client);

    Client getById(Long id);

    Client getByCardNumber(String cardNumber);

    int getDiscountPoints(Long id);

    void deleteClient(Long id);

    List<Client> getAll();
}
@Service
public class ClientServiceImpl implements ClientService {

    @Autowired
    private ClientRepository clientRepository;

    @Override
    public void addClient(Client client) {
        clientRepository.save(client);
    }

    @Override
    public Client getById(Long id) {
        return clientRepository.getReferenceById(id);
    }

    public Client getByCardNumber(String cardNumber) {
        return clientRepository.getByCardNumber(cardNumber);
    }

    @Override
    public int getDiscountPoints(Long id) {
        return clientRepository.getReferenceById(id).getDiscountPoints();
    }

    @Override
    public void deleteClient(Long id) {
        clientRepository.deleteById(id);
    }

    @Override
    public List<Client> getAll() {
        return clientRepository.findAll();
    }
}

База:

databaseChangeLog:
  - changeSet:
      id: SET-1
      author: user
      changes:
        - createTable:
            tableName: clients
            columns:
              - column:
                  name: id
                  type: serial
                  constraints:
                    nullable: false
                    primaryKey: true
              - column:
                  name: card_number
                  type: varchar(20)
                  constraints:
                    nullable: false
              - column:
                  name: discount_points
                  type: bigint
                  constraints:
                    nullable: false
        - createTable:
            tableName: order_checks
            columns:
              - column:
                  name: id
                  type: serial
                  constraints:
                    nullable: false
                    primaryKey: true
              - column:
                  name: client_id
                  type: bigint
                  constraints:
                    nullable: false
                    foreignKeyName: fk_client_id
                    references: clients(id)
              - column:
                  name: amount
                  type: decimal
                  constraints:
                    nullable: false

В целом все запросы, делающие выборку из одной таблицы, отрабатываю корректно. Но появилась необходимость в выборке из двух таблиц. На этом фоне изменился репозиторий чеков:

@Repository
public interface CheckRepository extends JpaRepository<Check, Long> {
    List<Check> getAllByClientId(Long clientId);

    @Query(value = "SELECT clients.id, order_checks.id, clients.card_number, order_checks.amount"
            + " FROM clients, order_checks "
            + " WHERE clients.id = order_checks.client_id "
            + " AND clients.card_number = :cardNumber "
            , nativeQuery = true)
    List<ClientAndCheckDTO> getAllByCardNumber(@Param("cardNumber") String cardNumber);
}

Структура DTO:

@Getter
@Setter
@ToString
@AllArgsConstructor
public class ClientAndCheckDTO {

    private Long clientId;
    private Long checkId;
    private String cardNumber;
    private BigDecimal amount;
}

Application.properties:

spring.application.name=PAService
server.port=9966

spring.datasource.url=jdbc:postgresql://localhost:5432/PAService
spring.datasource.username=user
spring.datasource.password=1234
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database=postgresql
#spring.jackson.serialization.fail-on-empty-beans=false

spring.liquibase.change-log=classpath:/liquibase/changelog.yml
spring.liquibase.enabled=true
spring.liquibase.url=jdbc:postgresql://localhost:5432/PAService
spring.liquibase.user=user
spring.liquibase.password=1234

CheckRestController:

@RestController
@RequestMapping("/checks")
public class CheckRestController {
    @Autowired
    private CheckService checkService;

    @GetMapping("/cardnumber/{cardNumber}")
    public ResponseEntity<List<ClientAndCheckDTO>> getAllByCardNumber(@PathVariable("cardNumber") String cardNumber) {
        if(cardNumber == null) {
            return null;
        }

        List<ClientAndCheckDTO> clientAndCheckDTOs = this.checkService.getAllByCardNumber((cardNumber));
        if(clientAndCheckDTOs.isEmpty()) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }

        return new ResponseEntity<>(clientAndCheckDTOs, HttpStatus.OK);
    }
}

Get запрос в качестве параметра должен принимать String из 20 символов. В Postman отправляю запрос:

localhost:9966/checks/cardnumber/99998888777766665555

Получаю результат:

2024-05-04T22:12:30.270+07:00 ERROR 9157 --- [PAService] [nio-9966-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [ru.fev.accumulation.dto.ClientAndCheckDTO]] with root cause

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [ru.fev.accumulation.dto.ClientAndCheckDTO]

Если запрос:

localhost:9966/checks/cardnumber/"99998888777766665555"

То получаю ответ: 404 Not found. Соответствующие чеки и клиенты в базу заранее добавлены и запросами getAll выводятся корректно. Что делаю не так? Только учусь. Буду благодарен за любую помощь!


Ответы (1 шт):

Автор решения: Juher

Решено. https://www.baeldung.com/spring-data-jpa-query. Пункт 4.2. Получилось, что в nativeQuery нельзя делать выборку определенных столбцов.

@Query(value = "SELECT clients.*, order_checks.* "
            + " FROM clients, order_checks "
            + " WHERE clients.id = order_checks.client_id "
            + " AND clients.card_number = ?1 "
            , nativeQuery = true)
    List<Map<String, Object>> getAllByCardNumber(String cardNumber);
→ Ссылка