Search code examples
javajpaquerydsl

Join a many to many relation with QueryDSL and JPASQLQuery


I have the following entities:

@AllArgsConstructor
@EqualsAndHashCode(of = {"name"})
@Data
@NoArgsConstructor
@Entity
@Table(schema = "eat")
public class Pizza {

   @Id
   @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="pizza_id_seq")
   private Integer id;

   @NotNull       
   private String name;

   @NotNull
   @Positive
   private Double cost;

   @ManyToMany
   @JoinTable(schema = "eat",
              name = "pizza_ingredient",
              inverseJoinColumns = { @JoinColumn(name = "ingredient_id") })
   private Set<Ingredient> ingredients;

}


@AllArgsConstructor
@EqualsAndHashCode(of = {"name"})
@Data
@NoArgsConstructor
@Entity
@Table(schema = "eat")
public class Ingredient {

   @Id
   @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="ingredient_id_seq")
   private Integer id;

   @NotNull
   @Size(min=1, max=64)
   private String name;

}

I'm using JPASQLQuery object provided by QueryDSL (4.2.2) to create some native queries in PostgreSQL:

public JPASQLQuery<T> getJPASQLQuery() {
   return new JPASQLQuery<>(
      entityManager,
      PostgreSQLTemplates.builder().printSchema().build()
   );
}

The problem comes trying to use join functions, for example:

QIngredient ingredient = QIngredient.ingredient;
QPizza pizza = QPizza.pizza;

StringPath ingredientPath = Expressions.stringPath("ingredient");
StringPath pizzaPath = Expressions.stringPath("pizza");
NumberPath<Double> costPath = Expressions.numberPath(Double.class, "cost");
Expression rowNumber = SQLExpressions.rowNumber().over().partitionBy(ingredientPath).orderBy(costPath.desc()).as("rnk");

JPASQLQuery subQuery = getJPASQLQuery()
   .select(ingredient.name.as(ingredientPath), pizza.name.as(pizzaPath), pizza.cost.as(costPath), rowNumber)
   .from(pizza)
   // The error is in next innerJoin
   .innerJoin((SubQueryExpression<?>) pizza.ingredients, ingredient)
   .where(ingredient.name.in(ingredientNames));

If I keep the current innerJoin((SubQueryExpression<?>) pizza.ingredients, ingredient) I receive:

class com.querydsl.core.types.dsl.SetPath cannot be cast to class com.querydsl.core.types.SubQueryExpression

I cannot remove current (SubQueryExpression<?>) because innerJoin doesn't accept SetPathas parameter.

On the other hand, the following:

.from(pizza)               
.innerJoin(ingredient)

Doesn't work due to pizza_ingredient is not included in the generated query.

How can I use innerJoin in JPASQLQuery with a many to many relationship like above?


Solution

  • Basically, there are two main approaches trying to solve it:


    Include required native functions

    As suggest one QueryDSL developer here, replacing JPASQLQuery by JPA alternatives.


    Create required Path for many to many table

    First is important to add name property into every @Table annotation because internally is the one used by QueryDSL NativeSQLSerializer class to generate from and join clauses.

    So, for example:

    @Table(schema = "eat")
    public class Pizza ...
    

    Should be replaced by:

    @Table(name = "pizza", schema = "eat")
    public class Pizza ...
    

    Next, create for custom Path for the many to many table:

    RelationalPathBase<Object> pizzaIngredient = new RelationalPathBase<>(Object.class, "pi", "eat", "pizza_ingredient");
    NumberPath<Integer> pizzaIngredient_PizzaId = Expressions.numberPath(Integer.class, pizzaIngredient, "pizza_id");
    NumberPath<Integer> pizzaIngredient_IngredientId = Expressions.numberPath(Integer.class, pizzaIngredient, "ingredient_id");
    

    So the complete code would be:

    QIngredient ingredient = QIngredient.ingredient;
    QPizza pizza = QPizza.pizza;
    
    RelationalPathBase<Object> pizzaIngredient = new RelationalPathBase<>(Object.class, "pi", "eat", "pizza_ingredient");
    NumberPath<Integer> pizzaIngredient_PizzaId = Expressions.numberPath(Integer.class, pizzaIngredient, "pizza_id");
    NumberPath<Integer> pizzaIngredient_IngredientId = Expressions.numberPath(Integer.class, pizzaIngredient, "ingredient_id");
    
    StringPath ingredientPath = Expressions.stringPath("ingredient");
    StringPath pizzaPath = Expressions.stringPath( "pizza");
    NumberPath<Double> costPath = Expressions.numberPath(Double.class, "cost");
    
    Expression rowNumber = SQLExpressions.rowNumber().over().partitionBy(ingredientPath).orderBy(costPath.desc()).as("rnk");
    NumberPath<Long> rnk = Expressions.numberPath(Long.class, "rnk");
    
    SubQueryExpression subQuery = getJPASQLQuery()
       .select(ingredient.name.as(ingredientPath), pizza.name.as(pizzaPath), pizza.cost.as(costPath), rowNumber)
       .from(pizza)
       .innerJoin(pizzaIngredient).on(pizzaIngredient_PizzaId.eq(pizza.id))
       .innerJoin(ingredient).on(ingredient.id.eq(pizzaIngredient_IngredientId))
       .where(ingredient.name.in(ingredientNames));
    
    return getJPASQLQuery()
              .select(ingredientPath, pizzaPath, costPath)
              .from(
                  subQuery,
                  Expressions.stringPath("temp")
              )
              .where(rnk.eq(1l))
              .fetch();