Kueri Kriteria JPA - Cara Menghindari Gabungan Duplikat

Saya perlu membuat kueri kriteria dengan banyak gabungan kondisional dan klausa mana, dalam kasus seperti itu kode cenderung menjadi rumit dan dapat menghasilkan gabungan duplikat.

Misalnya saya memiliki struktur Tabel dan entitas JPA berikut:

ACCOUNT
      ACCOUNT_ID
      ACCOUNT_TYPE


PERSON
      NAME
      AGE
      ACCOUNT_ID ( FK TO ACCOUNT ) 
      ADDRESS_ID ( FK TO ADDRESS ) 

ADDRESS
      ADDRESS_ID
      LOCATION
      COUNTRY

Jadi dengan asumsi saya menggunakan implementasi metamodel statis untuk menerapkan kueri kriteria.

Ini adalah contoh kode yang salah yang dapat menghasilkan gabungan duplikat:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Account> cq = cb.createQuery(Account.class);

    cq.select(accountRoot).where(
     cb.and(
      cb.equal(accountRoot.join(Account_.person).get(Person_.name),"Roger"),
      cb.greaterThan(accountRoot.join(Account_.person).get(Person_.age),18),
      cb.equal(accountRoot.join(Account_.person)                                   
              .join(Person_.address).get(Address_.country),"United States")
      )
     )

     TypedQuery<Account> query = entityManager.createQuery(cq);
     List<Account> result = query.getResultList();

Kode di atas akan menghasilkan SQL dengan beberapa gabungan tabel yang sama:

 Select
        account0_.account_id as account1_2_,
        account0_.account_type as account2_2_
    from
        account account0_
    inner join
        person person1_
            on account0_.account_id=person1_.account_id
    inner join
        address address2_
            on person1_.address_id=address2_.address_id
    inner join
        person person3_
            on account0_.account_id=person3_.account_id
    inner join
        person person4_
            on account0_.account_id=person4_.account_id
    inner join
        person person5_
            on account0_.account_id=person5_.account_id
    inner join
        address address6_
            on person5_.address_id=address6_.address_id
    where
        person3_.name=?
        and person4_.age>18
        and address6_.country=?

Solusi sederhana adalah dengan menjaga instance Gabungan untuk digunakan kembali dalam beberapa predikat seperti itu:

   Root<Account> accountRoot = cq.from(Account.class);
   Join<Account,Person> personJoin= accountRoot.join(Account_.person);
   Join<Person,Address> personAddressJoin = accountRoot.join(Person_.address);

   cq.select(accountRoot).where(
     cb.and(
      cb.equal(personJoin.get(Person_.name),"Roger"),
      cb.greaterThan(personJoin.get(Person_.age),18),
      cb.equal(personAddressJoin.get(Address_.country),"United States")
      )
     )

Oke, ini berhasil, Tetapi dengan kode yang sangat kompleks dengan beberapa tabel dan gabungan kondisional untuk kode tersebut cenderung menghasilkan kode Spaghetti! Percaya saya !

Apa cara yang lebih baik untuk menghindarinya?


person Eduardo Fabricio    schedule 21.07.2015    source sumber
comment
Anda memiliki banyak gabungan di SQL karena kode Anda memiliki banyak panggilan berlebihan ke join. Apa yang Anda harapkan akan terjadi? Panggil join sekali untuk setiap gabungan yang benar-benar Anda perlukan, dan gunakan kembali instance Join yang dikembalikan. Anda sudah membuat dua Join instance yang Anda perlukan - gunakan saja!   -  person Rob    schedule 21.07.2015
comment
@Rob Maaf, terima kasih atas komentar Anda, saya baru saja memposting pertanyaan yang tidak lengkap kemarin, sudah saya perbaiki. Saya tahu itu salah, Dalam pengalaman pertama saya dengan kueri kriteria dan penggabungan, saya membuat kesalahan itu dan saya seharusnya memposting dua kode yang salah dan memperbaikinya. Pertanyaan Diperbaiki! Namun menurut saya jawaban itu tidak salah, Ini hanya saran untuk menghindarinya dalam kueri kriteria yang kompleks, saya memiliki pengalaman yang baik dengan strategi ini dalam pekerjaan saya, jadi saya ingin membagikan solusi ini, tapi oke, jika itu milik Anda pendapat.   -  person Eduardo Fabricio    schedule 22.07.2015


Jawaban (2)


Saran untuk menghindarinya adalah dengan menggunakan kelas pembangun untuk merangkum gabungan, lihat di bawah.

public class AccountCriteriaBuilder {

        CriteriaBuilder cb;
        CriteriaQuery<Account> cq;

        // JOINS INSTANCE
        Root<Account> accountRoot;
        Join<Account,Person> personJoin;
        Join<Person,Address> personAddressJoin;

        public AccountCriteriaBuilder(CriteriaBuilder criteriaBuilder) {
            this.cb =  criteriaBuilder;
            this.cq = cb.createQuery(Account.class);
            this.accountRoot = cq.from(Account.class);
        }

        public CriteriaQuery buildQuery() {
            Predicate[] predicates = getPredicates();
            cq.select(accountRoot).where(predicates);
            return cq;
        }

        public Predicate[] getPredicates() {

           List<Predicate> predicates = new ArrayList<Predicate>();

           predicates.add(cb.equal(getPersonJoin().get(Person_.name), "Roger"));
           predicates.add(cb.greaterThan(getPersonJoin().get(Person_.age), 18));
           predicates.add(cb.equal(getPersonAddressJoin().get(Address_.country),"United States"));

           return predicates.toArray(new Predicate[predicates.size()]);
        }

        public Root<Account> getAccountRoot() {
            return accountRoot;
        }

        public Join<Account, Person> getPersonJoin() {
            if(personJoin == null){
                personJoin = getAccountRoot().join(Account_.person);
            }
            return personJoin;
        }

        public Join<Person, Address> getPersonAddressJoin() {
            if(personAddressJoin == null){
                personAddressJoin = getPersonJoin().join(Person_.address);
            }
            return personAddressJoin;
        }


}

“Ace in the hole” adalah pemuatan lambat untuk setiap instance join yang diperlukan, ini akan menghindari duplikat join dan juga untuk menyederhanakan proses navigasi.

Terakhir, panggil saja pembuatnya seperti di bawah ini:

AccountCriteriaBuilder criteriaBuilder = new AccountCriteriaBuilder(em.getCriteriaBuilder());
TypedQuery<Account> query = em.createQuery(criteriaBuilder.buildQuery());
List<Account> result = query.getResultList();

Menikmati :)

person Eduardo Fabricio    schedule 21.07.2015
comment
Untuk melihatnya berjalan, saya memiliki contoh implementasi lengkap dengan H2 di database memori dan jetty yang disematkan.. Cukup kloning dan jalankan github.com/dufabricio/hibernate-sample - person Eduardo Fabricio; 21.07.2015
comment
Kurasa aku tidak mengerti. Solusi sederhana Anda adalah 8 baris kode. Solusi Builder Anda berupa 30 baris kode untuk melakukan hal yang sama. Anda tidak dapat membenarkan hal ini kecuali kompleksitas ekstra dapat ditarik ke dalam kelas dasar yang digunakan oleh banyak entitas atau diamortisasi melalui banyak kueri berbeda. Menurut saya, menggunakan pola demi menggunakan pola adalah anti pola. - person Rob; 22.07.2015
comment
Baiklah saya setuju ! kode contoh ini bukan kode spaguetti asli, saya hanya membuat contoh untuk dijelaskan.. ini bukan kasus sebenarnya.. Tapi, @Rob, saya akan mencoba membuat contoh yang lebih mirip dengan kode asli saya untuk membenarkannya dengan lebih baik .. itu adalah bagian luar biasa dari internet.. Kebebasan Berbicara! - person Eduardo Fabricio; 22.07.2015
comment
Saya memiliki beberapa momen dalam hidup saya di mana saya banyak memikirkan tentang konsep, Pola, anti pola, dll. Bagi saya, hari ini.. aplikasi harus berfungsi dengan baik. .hanya itu ! ;) .. Sekali lagi itu hanya pendapat saya.. ! sekali lagi terima kasih dengan berkontribusi bersama Anda! Komentar Anda menunjukkan kepada saya bahwa sampel ini tidak begitu jelas. - person Eduardo Fabricio; 22.07.2015
comment
Apakah ada cara untuk memaksa Fetch menggunakan kembali gabungan yang ada? - person Glapa; 09.03.2017

Kami menggunakan metode utilitas berikut untuk menghindari duplikat gabungan

public class CriteriaApiUtils {
  public static <X, Y> ListJoin<X, Y> join(Root<X> criteriaRoot,
                                             ListAttribute<? super X, Y> attribute,
                                             JoinType joinType
  ) {
    return (ListJoin<X, Y>) criteriaRoot.getJoins().stream()
        .filter(j -> j.getAttribute().getName().equals(attribute.getName()) && j.getJoinType().equals(joinType))
        .findFirst()
        .orElseGet(() -> criteriaRoot.join(attribute, joinType));
  }
}
person user1096250    schedule 21.09.2020
comment
Saya menggunakan sesuatu yang serupa - ini jauh lebih tidak invasif dibandingkan membuat pembuat dan dapat digunakan kembali untuk semua jenis kueri kriteria. - person matt forsythe; 19.11.2020