Using arbitrary query as projection in spring data rest project

倾然丶 夕夏残阳落幕 提交于 2020-04-11 12:43:06

问题


How it is possible to use arbitrary sql query (I mean native sql query) in some repository? My actual problem is this:

@Data //lombok thing
@Entity
public class A extends AuditModel {
  private long id;
  private String name;

  @OneToMany(mappedBy="a") //Comments.a is owning side of association, i.e. comments table does have column called a_id as foreign key
  @ToString.Exclude
  private Set<Comments> comments = new HashSet();

  @OneToMany(mappedBy="a") //SimpleFile.a is owning side of association
  private Set<SimpleFile> comments = new HashSet();
}

Than I have my repository, which exposes nice crud interface using HAL+json representation. I am trying to enrich it with some projection/view particularly due to web UI to load one page data in single request. I am aware of excerps and projections, but they seems not to be enough powerful.

@Repository
@RepositoryRestResource
@Transactional(readOnly = true)
public interface ARepository extends PagingAndSortingRepository<A, Long> {
  Page<A> findByNameContaining(String namePart, Pageable pageable);
  @Query(
    value = "SELECT a.name,\n" +
      "(SELECT CAST(count(ac.id) AS int) FROM COMMENTS ac WHERE ac.a_id = a.id),\n" +
      "(SELECT listagg(asf.id) FROM SIMPLE_FILES asf WHERE asf.a_id = a.id)\n" +
      "FROM AS a\n" +
      "WHERE a.id = :id",
    nativeQuery = true
  )
  Optional<ACustomPage42DTO> getByIdProjectedForScreen42(Long id);
}

I have also tried to use JPQL, but there I had problem with fetch join (as I am not familiar with JPQL). My last evaluation query was something like this:

@Query("SELECT new sk.qpp.qqq.documents.projections.ACustomPage42DTO(" +
  "a " +
  "(SELECT CAST(count(ac) AS int) FROM COMMENTS ac WHERE ac.a = a)" +
  ")\n" +
  "FROM A a\n" +
  "LEFT JOIN FETCH a.simpleFiles\n" +
  "WHERE a.id = :id"
)

I would like to get some general advice about what approach is best to implement custom and complex query to be returned in DTO (ideally with some specific links to actions when needed).

PS: Implementing interface and returning simple (primitive) data works. Also using JPQL to create custom DAO instance works (with simple types and with single instance of type A for example). Method for using given query method does appear in search methods of given entity endpoint. I would like to have something more reasonable, so I would like to have projection as defined in spring data rest project.

I have my DTO object fully under my control. I prefer it to use @Value or @Data annotation from project lombok, but it is not a need. I have tried also these versions of DTO definition (using interface works for simple data and similarly class works for simple data).

interface ACustomPage42DTO {
    String getName();
    long getCommentsCount();
    Object getAsdf();
}

Or using equivalent class with some bonus, like custom toString() method possible, or some custom getter for computed data:

@Value //lombok thing, imutable "POJO"
public class ACustomPage42DTO {
    String name;
    long commentsCount;
    Set<SimpleFile> simpleFiles;
    public ACustomPage42DTO(A a, long count) {
        // constructor used by JPQL, if it works
        name = a.getName();
        this.commentsCount = count;
        this.simpleFiles = a.getSimpleFiles(); // should be already fetched, due to fetch join in JPQL
    }
}

Both working approaches can be called using "search" url, instead of projection. I see my method getByIdProjectedForScreen42 on url http://localhost:9091/api/a/search listing. I would like to use it like (I think that is the "right" way) http://localhost:8080/api/a?projection=ACustomPage42DTOProjection .


回答1:


Question is quite broad and touches couple of aspects:

  • custom JPA repository method using @Query
  • selecting results in your @Query
  • mapping @Query results to an interface
  • exposing new repository method through @RepositoryRestResource

TLDR: wrote an example of what is talked about with couple of basic tests https://github.com/ivarprudnikov/test-spring-jpa-repository-query-exposed-through-http

custom JPA repository method using @Query

As you have mentioned it is quite straightforward, just annotate a method with @Query and make sure your return type corresponds to what is being returned from the query, eg:

public interface FooRepository extends JpaRepository<FooEntity, Long> {
    @Query(nativeQuery = true, value = "select f from foo f where f.name = :myParam")
    Optional<FooEntity> getInSomeAnotherWay(String myParam);
}

selecting results in your @Query

You have given an example already but I'll simplify to make it easier and shorter.

Given entities FooEntity.java and BarEntity.java:

@Entity
@Table(name = "foo")
public class FooEntity {

    @Id
    @Column(name = "id", unique = true, nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    @OneToMany(mappedBy = "foo")
    private Set<BarEntity> bars = new HashSet<>();

    // getter setters excluded for brevity
}

@Entity
@Table(name = "bar")
public class BarEntity {

    @Id
    @Column(name = "id", unique = true, nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    @ManyToOne(targetEntity = FooEntity.class)
    @JoinColumn(name = "foo_id", nullable = false, foreignKey = @ForeignKey(name = "fk_bar_foo"))
    private FooEntity foo;

    // getter setters excluded for brevity
}

We want now to return custom result set which contains FooEntity.name and count of FooEntity.bars:

SELECT f.name as name, count(b.id) as barCount FROM foo f, bar b WHERE f.id = :id AND b.foo_id = :id


+-----------------+----------+
| name            | barCount |
+-----------------+----------+
| Jonny tables    | 1        |
+-----------------+----------+

mapping @Query results to an interface

To map above result set we need an interface where getters nicely reflect what is being selected:

public interface ProjectedFooResult {
    String getName();
    Long getBarCount();
}

Now we can rewrite our repository method to:

@Query(nativeQuery = true, 
    value = "SELECT f.name as name, count(b.id) as barCount FROM foo f, bar b WHERE f.id = :id AND b.foo_id = :id")
Optional<ProjectedFooResult> getByIdToProjected(Long id);

exposing new repository method through @RepositoryRestResource

I am not very familiar with this but after adding org.springframework.data:spring-data-rest-hal-browser dependency I got this nice interface that exposed available methods after repository was annotated with @RepositoryRestResource. For a given repository which contains above mentioned details:

@RepositoryRestResource(path = "foo")
public interface FooRepository extends JpaRepository<FooEntity, Long> {
    @Query(nativeQuery = true, value = "SELECT f.name as name, count(b.id) as barCount FROM foo f, bar b WHERE f.id = :id AND b.foo_id = :id")
    Optional<ProjectedFooResult> getByIdToProjected(Long id);
}

the method will be exposed through http://localhost:8080/foo/search/getByIdToProjected?id=1 when running locally.

As mentioned above the reference implementation is on Github https://github.com/ivarprudnikov/test-spring-jpa-repository-query-exposed-through-http

Additional helpful documentation for 'Custom Implementations for Spring Data Repositories'



来源:https://stackoverflow.com/questions/60622337/using-arbitrary-query-as-projection-in-spring-data-rest-project

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!