silentsudo
silentsudo

Reputation: 6973

JPA Join Query(native query) custom dto

Hi I am trying to join 3 table and fetch the collective result i.e dto using following query

SELECT f.id, u.email,
count(distinct l.id) as likes_count,
count(distinct c.id) as comments_count
FROM feeds f 
INNER JOIN users u ON f.user_id = u.id
INNER JOIN likes l on l.feed_id = f.id 
left join comments c on c.feed_id = f.id
WHERE u.id = 12055
group by f.id order by comments_count asc;

This query is working fine in mysql workbench, when i try to add this query to @Query annotation i am geeting AbstractJpaQuery$TupleConverter$TupleBackedMap exception, solution to this was to use ContructorExpression i added new expression but i am not getting it worked, my db entities are as below

@Entity
@Table(name = "feeds")
@NoArgsConstructor
public class Feed {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Getter
    @Setter
    Long id;

    @Getter
    @Setter
    @Column(columnDefinition = "TEXT")
    String content;

    public Feed(String content, User user) {
        this.content = content;
        this.user = user;
    }

    @ManyToOne(optional = false, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @Getter
    @Setter
    User user;


    @OneToMany(mappedBy = "feed", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @Getter
    @Setter
    List<Like> likes;


    @OneToMany(mappedBy = "feed", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @Getter
    @Setter
    List<Comment> comments;

    @Override
    public String toString() {
        return "Feed{" +
                "id=" + id +
                ", user=" + user +
                '}';
    }
}

Custom DTO Object for above sql query:

@NoArgsConstructor
@AllArgsConstructor
public class FeedDetails {

    @Getter
    @Setter
    private Long id;


    @Getter
    @Setter
    private Long likes;

    @Getter
    @Setter
    private Long comments;


}

Repository method as below:

@Query("**sql_query**")
List<FeedDetails> findAllFeedsByUserId(Long userId);

I am not sure what shout i write in sql_query such that i get proper FeedDetails List result.

Upvotes: 0

Views: 2596

Answers (2)

silentsudo
silentsudo

Reputation: 6973

Thank you for quick answer, i have taken reference from your answer both approaches were correct. Posting my answer here with respect to question:

Feed.java

@Entity
@Table(name = "feeds")
@SqlResultSetMapping(name = "findAllFeedByUserIdMapping",
        classes = @ConstructorResult(
                targetClass = FeedDetailsClass.class,
                columns = {
                        @ColumnResult(name = "id", type = Long.class),
                        @ColumnResult(name = "likes", type = Long.class),
                        @ColumnResult(name = "comments", type = Long.class)
                }
        )
)
@NamedNativeQuery(name = "findAllFeedByUserIdNamedQuery",
        resultClass = FeedDetails.class, resultSetMapping ="findAllFeedByUserIdMapping",
        query = "SELECT f.id,count(distinct l.id) as likes, count(distinct c.id) as comments FROM feeds f INNER JOIN users u ON f.user_id = u.id INNER JOIN likes l on l.feed_id = f.id left join comments c on c.feed_id = f.id WHERE u.id = 12055 group by f.id order by comments asc")

@NoArgsConstructor
public class Feed {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Getter
    @Setter
    Long id;

    @Getter
    @Setter
    @Column(columnDefinition = "TEXT")
    String content;

    public Feed(String content, User user) {
        this.content = content;
        this.user = user;
    }

    @ManyToOne(optional = false, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @Getter
    @Setter
    User user;


    @OneToMany(mappedBy = "feed", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @Getter
    @Setter
    List<Like> likes;


    @Fetch(value = FetchMode.JOIN)
    @OneToMany(mappedBy = "feed", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @Getter
    @Setter
    List<Comment> comments;

    @Override
    public String toString() {
        return "Feed{" +
                "id=" + id +
                ", user=" + user +
                '}';
    }
}

Repository

@Query(value = "SELECT f.id, f.content, u.name, u.email, " +
            "count(distinct l.id) as likes, " +
            "count(distinct c.id) as comments " +
            "FROM feeds f " +
            "INNER JOIN users u ON f.user_id = u.id " +
            "INNER JOIN likes l on l.feed_id = f.id " +
            "left join comments c on c.feed_id = f.id " +
            "WHERE u.id = 12055 " +
            "group by f.id " +
            "order by comments asc",
            nativeQuery = true)
    List<FeedDetails> findAllFeedsByUserId(Long userId);


    @Query(nativeQuery = true, name = "findAllFeedByUserIdNamedQuery")
    List<FeedDetailsClass> findAllFeedByUserIdNamedQuery(Long userId);

Interface approach works with native query findAllFeedsByUserId

public interface FeedDetails {

    Long getId();

    String getContent();

    String getEmail();

    String getName();

    Long getLikes();

    Long getComments();
}

Model Class approach works well with SqlResultSetMapping method findAllFeedByUserIdNamedQuery, also i had to mention type of the column explicitly

@Data
@NoArgsConstructor
public class FeedDetailsClass {

    @Getter
    @Setter
    private Long id;

    @Getter
    @Setter
    private Long likes;

    @Getter
    @Setter
    private Long comments;

    public FeedDetailsClass(Long id, Long likes, Long comments) {
        this.id = id;
        this.likes = likes;
        this.comments = comments;
    }
}

Upvotes: 1

Simon Martinelli
Simon Martinelli

Reputation: 36223

You cannot use constructor expression (NEW operator) with Native Queries.

You could use JPA constructor result. This looks like:

Query q = em.createNativeQuery(
    "SELECT c.id, c.name, COUNT(o) as orderCount, AVG(o.price) AS avgOrder " +
    "FROM Customer c " +
    "JOIN Orders o ON o.cid = c.id " +
    "GROUP BY c.id, c.name",
    "CustomerDetailsResult");

@SqlResultSetMapping(name="CustomerDetailsResult",
    classes={
        @ConstructorResult(targetClass=com.acme.CustomerDetails.class,
            columns={
                @ColumnResult(name="id"),
                @ColumnResult(name="name"),
                @ColumnResult(name="orderCount"),
                @ColumnResult(name="avgOrder", type=Double.class)})
    })

Or you could convert FeedDetails to an interface and try Spring Data JPA Interface projection: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections

Or if you don't like these option there is a little library called QLRM: https://github.com/simasch/qlrm/blob/master/ConstructorResult.md

Upvotes: 2

Related Questions