แบบสอบถามเกณฑ์ JPA - วิธีหลีกเลี่ยงการรวมที่ซ้ำกัน

ฉันจำเป็นต้องสร้างแบบสอบถามตามเกณฑ์ที่มีการรวมแบบมีเงื่อนไขจำนวนมากและส่วนคำสั่งต่างๆ ในกรณีเช่นนี้ โค้ดมีแนวโน้มที่จะซับซ้อนและอาจทำให้เกิดการรวมที่ซ้ำกัน

ตัวอย่างเช่นฉันมีโครงสร้างของ Tables และเอนทิตี JPA ดังต่อไปนี้:

ACCOUNT
      ACCOUNT_ID
      ACCOUNT_TYPE


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

ADDRESS
      ADDRESS_ID
      LOCATION
      COUNTRY

สมมติว่าฉันกำลังใช้การใช้งาน metamodel แบบคงที่สำหรับการใช้แบบสอบถามตามเกณฑ์

นี่คือตัวอย่างของรหัสที่ไม่ถูกต้องซึ่งสามารถสร้างการรวมที่ซ้ำกัน:

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();

โค้ดด้านบนจะสร้าง SQL พร้อมการรวมหลายรายการของตารางเดียวกัน:

 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=?

วิธีแก้ปัญหาง่ายๆ คือเก็บอินสแตนซ์ของ Joins ไว้เพื่อนำมาใช้ซ้ำในเพรดิเคตทวีคูณดังนี้:

   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")
      )
     )

ตกลง มัน ได้ผล แต่ ด้วย รหัส ที่ ซับซ้อน จริง ๆ ซึ่ง มี หลาย ตาราง และ การ รวม แบบ มีเงื่อนไข สำหรับ รหัส ก็ มีแนวโน้มที่จะ เปลี่ยน รหัส สปาเก็ตตี้ ! เชื่อฉัน !

อะไรจะดีไปกว่าการหลีกเลี่ยงมัน?


person Eduardo Fabricio    schedule 21.07.2015    source แหล่งที่มา
comment
คุณมีการรวมหลายครั้งใน SQL เนื่องจากโค้ดของคุณมีการเรียก join ซ้ำซ้อนหลายครั้ง คุณคาดหวังว่าจะเกิดอะไรขึ้น? โทร join หนึ่งครั้งสำหรับการเข้าร่วมแต่ละครั้งที่คุณต้องการจริง และใช้อินสแตนซ์ Join ที่ส่งคืนอีกครั้ง คุณสร้าง Join อินสแตนซ์ทั้งสองที่คุณต้องการแล้ว - เพียงใช้มัน!   -  person Rob    schedule 21.07.2015
comment
@Rob ขออภัย tx สำหรับความคิดเห็นของคุณ ฉันเพิ่งโพสต์คำถามที่ไม่สมบูรณ์เมื่อวานนี้ ฉันแก้ไขแล้ว ฉันรู้ว่ามันผิด ในประสบการณ์ครั้งแรกของฉันกับแบบสอบถามเกณฑ์และเข้าร่วม ฉันทำผิดพลาดนั้น และฉันควรจะโพสต์รหัสทั้งสองผิดและแก้ไขแล้ว ตอบคำถามแล้ว! อย่างไรก็ตาม ฉันไม่คิดว่าคำตอบนั้นผิด มันเป็นเพียงข้อเสนอแนะเพื่อหลีกเลี่ยงในการสืบค้นเกณฑ์ที่ซับซ้อน ฉันมีประสบการณ์ที่ดีกับกลยุทธ์นี้ในงานของฉัน ดังนั้นฉันจึงต้องการแบ่งปันวิธีแก้ปัญหานี้ แต่ตกลง ถ้าเป็นของคุณ ความคิดเห็น.   -  person Eduardo Fabricio    schedule 22.07.2015


คำตอบ (2)


คำแนะนำในการหลีกเลี่ยงคือการใช้คลาสตัวสร้างเพื่อห่อหุ้มการรวม ดูด้านล่าง

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;
        }


}

“เอซในรู” คือการโหลดแบบ Lazy สำหรับแต่ละอินสแตนซ์การรวมที่จำเป็น โดยจะหลีกเลี่ยงการรวมซ้ำ และยังทำให้กระบวนการนำทางง่ายขึ้นอีกด้วย

สุดท้าย เพียงโทรหาผู้สร้างดังนี้:

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

สนุก :)

person Eduardo Fabricio    schedule 21.07.2015
comment
เพื่อให้เห็นว่ามันทำงานอยู่ ฉันมีตัวอย่างการใช้งานที่สมบูรณ์ด้วย H2 ในฐานข้อมูลหน่วยความจำและท่าเทียบเรือแบบฝัง .. เพียงแค่โคลนและรัน github.com/dufabricio/hibernate-sample - person Eduardo Fabricio; 21.07.2015
comment
ฉันเดาว่าฉันไม่เข้าใจมัน วิธีแก้ปัญหาง่ายๆ ของคุณคือโค้ด 8 บรรทัด โซลูชัน Builder ของคุณมีโค้ดประมาณ 30 บรรทัดเพื่อทำสิ่งเดียวกัน คุณไม่สามารถพิสูจน์สิ่งนี้ได้ เว้นแต่จะสามารถดึงความซับซ้อนพิเศษมาสู่คลาสพื้นฐานที่หลายเอนทิตีใช้หรือตัดจำหน่ายสำหรับการสืบค้นที่แตกต่างกันจำนวนมาก ในความคิดของฉัน การใช้รูปแบบเพื่อประโยชน์ของการใช้รูปแบบถือเป็นการต่อต้านรูปแบบ - person Rob; 22.07.2015
comment
ตกลงฉันเห็นด้วย ! โค้ดตัวอย่างนี้ไม่ใช่โค้ดสปาเก็ตตี้จริง ฉันแค่สร้างตัวอย่างเพื่ออธิบาย .. มันไม่ใช่กรณีจริง .. แต่ชี้ให้เห็นชัดเจน @Rob ฉันจะพยายามสร้างตัวอย่างให้คล้ายกับโค้ดจริงของฉันมากขึ้นเพื่อปรับให้เหมาะสมยิ่งขึ้น .. มันเป็นส่วนที่ยอดเยี่ยมของอินเทอร์เน็ต.. เสรีภาพในการพูด ! - person Eduardo Fabricio; 22.07.2015
comment
ฉันมีช่วงเวลาในชีวิตที่ฉันคิดมากเกี่ยวกับแนวคิด รูปแบบ รูปแบบการต่อต้าน ฯลฯ เหล่านี้ สำหรับฉัน วันนี้ .. แอปต้องทำงานได้ดี .แค่มัน! ;) .. อีกครั้งเป็นเพียงความคิดเห็นของฉัน.. ! ขอขอบคุณอีกครั้งโดยมีส่วนร่วมกับคุณ ! ความคิดเห็นของคุณแสดงให้ฉันเห็นว่าตัวอย่างนี้ไม่ชัดเจนนัก - person Eduardo Fabricio; 22.07.2015
comment
มีวิธีบังคับให้ Fetch ใช้การเข้าร่วมที่มีอยู่ซ้ำหรือไม่ - person Glapa; 09.03.2017

เราใช้วิธีการอรรถประโยชน์ต่อไปนี้เพื่อหลีกเลี่ยงการรวมซ้ำ

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
ฉันใช้สิ่งที่คล้ายกัน - การสร้างตัวสร้างนั้นไม่รุกรานมากนักและสามารถนำมาใช้ซ้ำได้สำหรับการสืบค้นเกณฑ์ประเภทใดก็ได้ - person matt forsythe; 19.11.2020