Избегайте вычислений в памяти для OneToMany ... при получении MAX 1 Child - PullRequest
0 голосов
/ 08 декабря 2018

Я использую Spring Data JPA, и у меня есть объекты, которые отображаются в соответствующие таблицы.Мне нужно запросить результаты, чтобы я мог выбрать всех родителей и одного ребенка на каждого родителя на основе их strength.

@Entity
public class Parent {
  @Id
  Long id;

  @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
  private List<Child> children;
}

@Entity
public class Child {
  @Id
  Long id;

  @Enumerated(EnumType.STRING)
  private Strength strength;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "parent_id", nullable = false, insertable = true, updatable = false)
  private Parent parent;
} 

public Enum Strength {
  STRONG,
  NORMAL,
  WEAK
}

У меня есть базовый репозиторий crud:

@Repository
public interface ParentRepository extends CrudRepository<Parent, Long>{}

Некоторые правила и предположения:

  1. Ребенок принадлежит одному родителю
  2. У родителя может быть много дочерних объектов в БД
  3. У родителя может быть 0 или 1 дочерний элемент, имеющий силу = STRONG
  4. У родителя будет 1 дочерний элемент, который имеет силу = NORMAL
  5. У родителя будет 0 или более дочерних объектов, имеющих силу = WEAK
  6. Слабый дочерний элемент никогда не возвращается
  7. Приведенный ниже метод getParentAndStrongChildren должен возвращать максимум 1 дочерний объект.

Я могу сделать запрос findAll для метода родительского репозитория в Spring и затем отобразить результаты в памяти примерно так:

public List<Parent> getParentAndStrongChildren(){
    List<Parent> parents = parentRepository
        .findAll().stream()
        .map(p -> {
            if(p.getChildren() != null && p.getChildren.size() > 1){
               Child found = p.getChildren().stream()
                            .filter(c -> c.getStrength() == Strength.STRONG)
                            .findFirst()
                            .orElseGet(()-> p.getChildren().stream()
                                             .filter(c -> c.getStrength() == Strength.NORMAL)
                                             .findFirst()   
                                             .orElse(null));
                p.setChildren(found == null ? null : new Arrays.asList(found));
            }
        }
    return parents;
}

Q: Есть лилюбой способ не делать фильтры в памяти и полагаться на JPQL и аннотацию @Query для достижения этой цели?

1 Ответ

0 голосов
/ 18 апреля 2019

Это типичный SQL-запрос " top N на категорию ".Я лично сомневаюсь, что это можно сделать с помощью JPQL, но, возможно, кто-то еще даст ответ.Вот стандартное решение SQL вашей проблемы с использованием бокового:

SELECT p.*
FROM parent p
LEFT JOIN LATERAL (
  SELECT c.*
  FROM child c
  WHERE p.id = c.parent_id
  AND c.strength != 'WEAK'
  ORDER BY CASE c.strength WHEN 'STRONG' THEN 1 WHEN 'NORMAL' THEN 2 END
  FETCH FIRST ROW ONLY
) c ON 1 = 1

В качестве альтернативы, используя оконные функции (также стандартный SQL):

SELECT p.*
FROM parent p
LEFT JOIN (
  SELECT c.*, row_number() OVER (
    PARTITION BY c.parent_id 
    ORDER BY CASE c.strength WHEN 'STRONG' THEN 1 WHEN 'NORMAL' THEN 2 END
  ) AS rk
  FROM child c
  WHERE c.strength != 'WEAK'
) c ON p.id = c.parent_id AND c.rk = 1
...