مرحبًا ، هذه هي المشاركة الثانية حول Spring Data JPA. الجزء الأول كان مكرسًا تمامًا للخداع تحت الماء ، بالإضافة إلى نصائح من ذوي الخبرة. في هذا الجزء سوف نتحدث عن كيفية صقل الإطار لاحتياجاتك. جميع الأمثلة الموضحة متاحة هنا .
التهم
لنبدأ ، ربما ، بمهمة بسيطة وفي نفس الوقت: عند تحميل كيان ، من الضروري تنزيل "ابنته" بشكل انتقائي. النظر في مثال بسيط:
@Entity public class Child { @Id private Long id; @JoinColumn(name = "parent_id") @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Parent parent; } @Entity public class Parent { @Id private Long id; }
الكيان الفرعي في مثالنا كسول: لا نريد تحميل بيانات غير ضرورية (والانضمام إلى جدول آخر في استعلام SQL) عند تلقي Child
. لكن في بعض الحالات في طلبنا ، نعرف بالتأكيد أننا سنحتاج إلى كل من الطفل ووالده. إذا تركت الكيان كسول ، فسنحصل على طلبين منفصلين. إذا طبقت عملية التحميل السريع عن طريق إزالة FetchType.LAZY
، فسيتم تحميل الكيانين دائمًا عند الطلب الأول (ونحن لا نريد ذلك).
يوفر JPQL حلاً جيدًا من خارج الصندوق - هذه هي الكلمة الرئيسية fetch
:
public interface ChildRepository extends JpaRepository<Child, Long> { @Query("select c from Child c join fetch c.parent where c.id = :id") Child findByIdFetchParent(@Param("id") Long id); }
هذا الطلب بسيط وواضح ، لكن به عيوب:
- لقد قمنا بالفعل بتكرار منطق
JpaRepository::findById
بإضافة تحميل صريح @Query
فحص كل استعلام تم وصفه باستخدام @Query
عند بدء تشغيل التطبيق ، مما يتطلب تحليل الاستعلام ، والتحقق من الوسائط ، وما إلى ذلك (راجع org.springframework.data.jpa.repository.query.SimpleJpaQuery :: validateQuery ). كل هذا عمل يستغرق وقتًا وذاكرة.- استخدام مثل هذا النهج في مشروع كبير مع عشرات المستودعات والكيانات المرتبطة (في بعض الأحيان مع عشرات من "بنات") سيؤدي إلى انفجار اندماجي.
التهم تأتي لمساعدتنا:
@Entity @NamedEntityGraphs(value = { @NamedEntityGraph( name = Child.PARENT, attributeNodes = @NamedAttributeNode("parent") ) }) public class Child { public static final String PARENT = "Child[parent]"; @Id private Long id; @JoinColumn(name = "parent_id") @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Parent parent; }
الرسم البياني نفسه سهل الوصف ؛ تبدأ الصعوبات عند استخدامه. Spring Data JPA في صفحتها تقترح القيام بذلك بهذه الطريقة (كما ينطبق على حالتنا):
public interface GroupRepository extends JpaRepository<GroupInfo, String> { @EntityGraph(value = Child.PARENT) @Query("select c from Child c where c.id = :id") Child findByIdFetchParent(@Param("id") Long id); }
نرى هنا جميع المشكلات نفسها (باستثناء أن الطلب المكتوب أصبح أسهل قليلاً). يمكنك وضع حد لها في ضربة واحدة بمساعدة صقل. قم بإنشاء الواجهة الخاصة بك ، والتي سوف نستخدمها لبناء مستودعات بدلاً من JpaRepository
المعبأة:
@NoRepositoryBean public interface BaseJpaRepository<T, ID extends Serializable> extends JpaRepository<T, ID> { T findById(ID id, String graphName); }
الآن التنفيذ:
public class BaseJpaRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements BaseJpaRepository<T, ID> { private final JpaEntityInformation<T, ?> entityInfo; private final EntityManager entityManager; public BaseJpaRepositoryImpl(JpaEntityInformation<T, ?> ei, EntityManager em) { super(ei, em); this.entityInfo = ei; this.entityManager = em; } @Override public T findById(ID id, String graphName) { Assert.notNull(id, "The given id must not be null!");
الآن يلزم الربيع باستخدام BaseJpaRepositoryImpl
كأساس لجميع مستودعات تطبيقنا:
@EnableJpaRepositories(repositoryBaseClass = BaseJpaRepositoryImpl.class) public class AppConfig { }
الآن ستكون طريقتنا متاحة من جميع المستودعات الموروثة من مستودع BaseJpaRepository
بنا.
هذا النهج له عيب واحد يمكن أن يضع خنزير سمين للغاية.
حاول أن تفكر بنفسكالمشكلة هي أن السبات (على الأقل وقت كتابة هذا التقرير) لا يتطابق مع أسماء الرسوم البيانية والرسوم البيانية نفسها. لهذا السبب ، قد يكون هناك خطأ في وقت التشغيل عندما نقوم بتنفيذ شيء من هذا القبيل
Optional<MyEntity> entity = repository.findById(id, NON_EXISTING_GRAPH);
يمكنك التحقق من صحة الحل باستخدام الاختبار:
@Sql("/ChildRepositoryGraphTest.sql") public class ChildRepositoryGraphTest extends TestBase { private final Long childId = 1L; @Test public void testGraph_expectFieldInitialized() { Child child1 = childRepository.findOne(childId, Child.PARENT); boolean initialized = Hibernate.isInitialized(child1.getParent()); assertTrue(initialized); } @Test public void testGraph_expectFieldNotInitialized() { Child child1 = childRepository .findById(childId) .orElseThrow(NullPointerException::new); boolean initialized = Hibernate.isInitialized(child1.getParent()); assertFalse(initialized); } }
عندما كانت الأشجار كبيرة
وكنا صغارًا وعديمي الخبرة ، غالبًا ما كان علينا رؤية هذا الرمز:
public List<DailyRecord> findBetweenDates(Date from, Date to) { StringBuilder query = new StringBuilder("from Record "); if (from != null) { query.append(" where date >=").append(format(from)).append(" "); } if (to != null) { if (from == null) { query.append(" where date <= " + format(to) + " "); } else { query.append(" and date <= " + format(to) + " "); } } return em.createQuery(query.toString(), DailyRecord.class).getResultList(); }
يجمع هذا الرمز قطعة الطلب قطعة. عيوب هذا النهج واضحة:
- عليك أن تفعل الكثير بيديك
- حيث يوجد عمل يدوي - هناك أخطاء
- لا تسليط الضوء على بناء الجملة (الأخطاء المطبعية في وقت التشغيل)
- من الصعب للغاية تمديد التعليمات البرمجية والحفاظ عليها
بعد ذلك بقليل ، ظهرت واجهة برمجة تطبيقات المعايير ، والتي سمحت لنا بضغط الكود أعلاه قليلاً:
public List<DailyRecord> findBetweenDates(Date from, Date to) { Criteria criteria = em .unwrap(Session.class) .createCriteria(DailyRecord.class); if (from != null) { criteria.add(Expression.ge("date", from)); } if (to != null) { criteria.add(Expression.le("date", to)); } return criteria.list(); }
استخدام المعايير له العديد من المزايا:
- القدرة على استخدام metamodel بدلاً من القيم "السلكية" مثل "date"
- تتحول بعض الأخطاء في إنشاء الطلب إلى أخطاء في الترجمة ، بمعنى أنه تم اكتشافها بالفعل عند الكتابة
- الرمز أقصر وأكثر وضوحًا من خلال سلاسل اللصق الغبية
هناك أيضا عيوب:
- رمز معقدة بما فيه الكفاية لفهم
- لمعرفة كيفية كتابة مثل هذه الاستعلامات ، يجب أن تملأ يدك (أتذكر ألمًا شديدًا عندما اضطررت للتعامل مع تصحيح الأخطاء لأول مرة في مثل هذه الاستعلامات ، التي تتكون أحيانًا من 100 إلى 150 سطرًا ، متفرعة ، إلخ)
- استعلام معقد مرهق إلى حد ما (50 سطرًا بعيدًا عن الحد الأقصى)
أريد تطويره بسهولة وبكل سرور ، لذلك أنا لا أحب كلتا الطريقتين.
دعنا ننتقل إلى الكيان الذي درسناه بالفعل:
@Entity public class Child { @Id private Long id; @JoinColumn(name = "parent_id") @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Parent parent;
أود أن أكون قادرًا على تحميل كيان في أوضاع مختلفة (ومجموعاتها):
- تحميل (أم لا) الأم
- تحميل (أو لا) اللعب
- فرز الأطفال حسب العمر
إذا قمت بحل هذه المشكلة وجهاً لوجه ، أي عن طريق كتابة الكثير من الاستعلامات التي تتوافق مع وضع التحميل المحدد ، فسيؤدي ذلك سريعًا إلى انفجار اندماجي:
@Query("select c from Child c join fetch c.parent order by c.age") List<Child> findWithParentOrderByAge(); @Query("select c from Child c join fetch c.toys order by c.age") List<Child> findWithToysOrderByAge(); @Query("select c from Child c join fetch c.parent join fetch c.toys") List<Child> findWithParentAndToys();
هناك طريقة بسيطة وأنيقة لحل هذه المشكلة: مزيج من محركات SQL / HQL والقالب. تم استخدام "Freemarker" في مشاريعي ، على الرغم من إمكانية استخدام حلول أخرى ("Timlif" ، "Mustash" ، وما إلى ذلك).
لنبدأ خلق. بادئ ذي بدء ، نحتاج إلى وصف الاستعلام في ملف يتلقى الامتداد *.hql.ftl
أو *.sql.ftl
(في حالة استخدام SQL "خالص"):
أنت الآن بحاجة إلى معالج:
@Component @RequiredArgsConstructor public class TemplateParser { private final Configuration configuration; @SneakyThrows public String prepareQuery(String templateName, Map<String, Object> params){ Template template = configuration.getTemplate(templateName); return FreeMarkerTemplateUtils.processTemplateIntoString(template, params); } }
لا شيء معقد. الوصول إلى المستودع. من الواضح أن الواجهة JpaRepository
لا JpaRepository
. بدلاً من ذلك ، سنستغل الفرصة لإنشاء مستودعاتنا الخاصة:
public interface ChildRepositoryCustom { List<Child> findAll(boolean fetchParent, boolean fetchToys, boolean order); } @RequiredArgsConstructor public class ChildRepositoryImpl extends BaseDao implements ChildRepositoryCustom { private final TemplateParser templateParser; @Override public List<Child> findAll(boolean fetchParent, boolean fetchToys, boolean order) { Map<String, Object> params = new HashMap<>(); params.put("fetchParent", fetchParent); params.put("fetchToys", fetchToys); params.put("orderByAge", orderByAge); String query = templateParser.prepareQuery(BASE_CHILD_TEMPLATE.name, params); return em.createQuery(query, Child.class).getResultList(); } @RequiredArgsConstructor enum RepositoryTemplates { BASE_CHILD_TEMPLATE("BaseChildTemplate.hql.ftl"); public final String name; } }
لجعل طريقة findUsingTemplate
يمكن الوصول إليها من ChildRepository
تحتاج إلى القيام بذلك:
public interface ChildRepository extends BaseJpaRepository<Child, Long>, ChildRepositoryCustom {
ميزة مهمة مرتبطة بالاسمسيقوم Spring بربط فصلنا وواجهاتنا فقط بالاسم الصحيح:
- ChildRepository
- تشايلدوسبيتوري مخصص
- مستودع إيداع الطفل
تذكر ذلك ، لأنه في حالة وجود خطأ في الاسم ، سيتم طرح استثناء غير مفهوم ، حيث يستحيل فهم سبب الخطأ.
الآن باستخدام هذا النهج يمكنك حل مشاكل أكثر تعقيدا. لنفترض أننا بحاجة إلى إجراء اختيار بناءً على الخصائص التي يحددها المستخدم. بمعنى آخر ، إذا لم يحدد المستخدم التواريخ "من" و "إلى" ، فلن يكون هناك وقت للتصفية. إذا تم تحديد التاريخ "من" أو فقط التاريخ "إلى" ، فسيتم التصفية في اتجاه واحد. إذا تم تحديد كلا التاريخين ، فسيتم تحديد السجلات بين التواريخ المحددة فقط في التحديد:
@Getter @RequiredArgsConstructor public class RequestDto { private final LocalDate from; private final LocalDate to; public boolean hasDateFrom() { return from != null; } public boolean hasDateTo() { return to != null; } } @Override public List<Child> findAll(ChildRequest request) { Map<String, Object> params = singletonMap("request", request); String query = templateParser.prepareQuery(TEMPLATE.name, params); return em.createQuery(query, Child.class).getResultList(); }
الآن القالب:
<
أوراكل و nvl
النظر في جوهر:
@Entity public class DailyRecord { @Id private Long id; @Column private String currency; @Column(name = "record_rate") private BigDecimal rate; @Column(name = "fixed_rate") private BigDecimal fxRate; @Setter(value = AccessLevel.PRIVATE) @Formula("select avg(r.record_rate) from daily_record r where r.currency = currency") private BigDecimal avgRate; }
يتم استخدام هذا الكيان في الاستعلام (DBMS ، كما نتذكر ، لدينا Oracle):
@Query("select nvl(record.fxRate, record.avgRate) " + " from DailyRecord record " + "where record.currency = :currency") BigDecimal findRateByCurrency(@Param("currency") String currency);
هذا طلب عمل صالح. ولكن هناك مشكلة واحدة صغيرة ، والتي ربما يشير إليها خبراء SQL. الحقيقة هي أن nvl
في أوراكل ليس كسول. بمعنى آخر ، عند استدعاء الأسلوب findRateByCurrency
، سيتم findRateByCurrency
سجل findRateByCurrency
select nvl( dr.fixed_rate, select avg(r.record_rate) from daily_record r where r.currency = dr.currency ) from daily_record dr where dr.currency = ?
حتى إذا كانت قيمة dr.fixed_rate
موجودة ، فإن نظام إدارة قواعد البيانات (DBMS) لا يزال يحسب القيمة التي يتم إرجاعها بواسطة التعبير الثاني في nvl
، وهو في حالتنا
select avg(r.record_rate) from daily_record r where r.currency = dr.currency)
ربما يعرف القارئ بالفعل كيف يراوغ الثقل غير الضروري للطلب: بالطبع ، هذه هي الكلمة الرئيسية nvl
، والتي تقارن بشكل إيجابي مع nvl
، وكذلك القدرة على قبول أكثر من تعبيرين. دعنا نصحح طلبنا:
@Query("select coalesce(record.fxRate, record.avgRate) " + " from DailyRecord record " + "where record.currency = :currency") BigDecimal findRateByCurrency(@Param("currency") String currency);
ثم ، كما يقولون ، فجأة:
select nvl(dr.fixed_rate, select avg(r.record_rate) from daily_record r where r.currency = dr.currency) from daily_record dr where dr.currency = ?
بقي الطلب كما هو. ذلك لأن لهجة أوراكل من الصندوق تتحول إلى سلسلة nvl
.
تعليقإذا كنت ترغب في إعادة إنتاج هذا السلوك ، فاحذف السطر الثاني في مُنشئ فئة CustomOracleDialect وقم بتشغيل DailyRecordRepositoryTest::findRateByCurrency
لتفادي هذا ، تحتاج إلى إنشاء لهجة خاصة بك واستخدامها في التطبيق:
public class CustomOracleDialect extends Oracle12cDialect { public CustomOracleDialect() { super(); registerFunction("coalesce", new StandardSQLFunction("coalesce")); } }
نعم ، هذا بسيط. الآن نربط اللهجة التي تم إنشاؤها بالتطبيق:
spring: jpa: database-platform: com.luxoft.logeek.config.CustomOracleDialect
طريقة أخرى (مهملة): spring: jpa: properties: hibernate.dialect: com.luxoft.logeek.config.CustomOracleDialect
التنفيذ المتكرر للطلب يعطي koalesk:
select coalesce(dr.fixed_rate, select avg(r.record_rate) from daily_record r where r.currency = dr.currency) from daily_record dr where dr.currency = ?
أوراكل وطلبات الصفحة
بشكل عام ، يوفر إكمال لهجة فرصًا ثرية لمعالجة الاستعلامات. غالبًا ما يتم تطوير مهمة ترحيل صفحات البيانات عند تطوير تطبيق ووجه ويب. بمعنى آخر ، لدينا عدة مئات الآلاف من السجلات في قاعدة البيانات ، لكن يتم عرضها في حزم من 10/50/100 سجل في كل صفحة. يوفر تاريخ النفاذ للخارج للمطوّر وظائف مماثلة:
@Query("select new com.luxoft.logeek.data.BriefChildData(" + "c.id, " + "c.age " + ") from Child c " + " join c.parent p " + "where p.name = ''") Page<BriefChildData> browse(Pageable pageable);
يحتوي هذا النهج على عيب كبير ، وهو تنفيذ استعلامين ، أولهما يحصل على البيانات ، والثاني يحدد إجمالي عددهم في قاعدة البيانات (هذا ضروري لعرض إجمالي كمية البيانات في كائن Page
). في حالتنا ، يعطي استدعاء لهذه الطريقة الطلبات التالية (التسجيل باستخدام p6spy):
select * from ( select c.id, c.age from child c inner join parent p on c.parent_id = p.id where p.name = '' ) where rownum <= 3; select count(c.id) from child c inner join parent p on c.parent_id = p.id where p.name = ''
إذا كان الاستعلام ثقيلًا (يتم ربط العديد من الجداول بعمود غير قابل للفهرسة ، فقط عدد كبير من الصلات ، شرط التحديد الصعب ، وما إلى ذلك) ، يمكن أن يصبح هذا مشكلة. ولكن نظرًا لأن لدينا Oracle ، فإنه باستخدام العمود الزائف rownum يمكنك الحصول على طلب واحد.
للقيام بذلك ، نحتاج إلى إنهاء لهجتنا ، ووصف الوظيفة المستخدمة لحساب جميع السجلات:
public class CustomOracleDialect extends Oracle12cDialect { public CustomOracleDialect() { super(); registerFunction("coalesce", new StandardSQLFunction("coalesce")); registerFunction("total_count", new TotalCountFunc()); } } public class TotalCountFunc implements SQLFunction { @Override public boolean hasArguments() { return true; } @Override public boolean hasParenthesesIfNoArguments() { return true; } @Override public Type getReturnType(Type type, Mapping mapping) { return StandardBasicTypes.LONG; } @Override public String render(Type type, List arguments, SessionFactoryImplementor factory) { if (arguments.size() != 1) { throw new IllegalArgumentException("Only 1 argument acceptable"); } return " count(" + arguments.get(0) + ") over () "; } }
اكتب الآن استعلامًا جديدًا (في فئة ChildRepositoryImpl
):
@Override public Page<BriefChildData> browseWithTotalCount(Pageable pageable) { String query = "select " + " c.id as id," + " c.age as age, " + " total_count(c.id) as totalCount" + " from Child c " + "join c.parent p " + "where p.name = ''"; List<BriefChildData> list = em.unwrap(Session.class) .createQuery(query) .setFirstResult((int) pageable.getOffset()) .setMaxResults(pageable.getPageSize()) .setResultTransformer(Transformers.aliasToBean(BriefChildData.class)) .getResultList(); if (list.isEmpty()) { return new PageImpl(Collections.emptyList()); } long totalCount = list.get(0).getTotalCount(); return new PageImpl<>(list, pageable, totalCount); }
عند استدعاء هذا الرمز ، سيتم تنفيذ طلب واحد
select * from (select c.id, c.age, count(c.id) over ()
باستخدام count(c.id) over ()
التعبير count(c.id) over ()
يمكنك الحصول على إجمالي كمية البيانات والحصول عليها من فئة البيانات PageImpl
مُنشئ PageImpl
. هناك طريقة للقيام بذلك بشكل أكثر أناقة ، دون إضافة حقل آخر إلى فئة البيانات ، فكر في أنه واجب منزلي :) يمكنك اختبار الحل باستخدام اختبار ProjectionVsDataTest .
مطبات التخصيص
لدينا مشروع رائع مع Oracle و Spring Date. مهمتنا هي تحسين أداء هذا الرمز:
List<Long> ids = getIds(); ids.stream() .map(repository::findById) .filter(Optional::isPresent) .map(Optional::get) .forEach(this::sendToSchool);
يكمن العيب على السطح: عدد استعلامات قاعدة البيانات يساوي عدد المفاتيح الفريدة. هناك طريقة معروفة للتغلب على هذه الصعوبة:
List<Long> ids = getIds(); repository .findAllById(ids) .forEach(this::sendToSchool);
ميزة أخذ العينات المتعددة واضحة: إذا كان لدينا في وقت سابق العديد من الاستفسارات المماثلة في النموذج
select p.* from Pupil p where p.id = 1 select p.* from Pupil p where p.id = 2 select p.* from Pupil p where p.id = 3
ثم الان انهاروا لواحد
select p.* from Pupil p where p.id in (1, 2, 3, ... )
يبدو أنه أصبح أسهل وأصبح جيدًا. ينمو المشروع ويتطور ويضاعف البيانات ويأتي وقت لا مفر منه:
آكي الرعد في اللون الأزرق ERROR - ORA-01795: maximum number of expressions in a list is 1000
نحن بحاجة إلى البحث عن مخرج مرة أخرى (وليس العودة إلى الإصدار القديم). نظرًا لأن Oracle لا يسمح له بتغذية أكثر من 1000 مفتاح ، فيمكنك تقسيم مجموعة البيانات بأكملها إلى مشاركات متساوية لا تزيد عن 1000 وتنفيذ عدد من الاستعلامات:
List<List<Long>> idChunks = cgccLists.partition(ids, 1000);
تعمل هذه الطريقة ، لكنها تنبعث منها رائحة طفيفة (هل هي كذلك؟): إذا واجهت مثل هذه الصعوبات في أماكن أخرى ، فعليك أن تقوم بسياج الحديقة نفسها. دعونا نحاول حل المشكلة بشكل أكثر أناقة ، أي من خلال BaseJpaRepositoryImpl
. أسهل طريقة للقيام بذلك هي نقل المنطق أعلاه إلى الداخل ، وإخفائه عن المستخدم:
@Override public List<T> findAllById(Iterable<ID> ids) { Assert.notNull(ids, "The given Iterable of Id's must not be null!"); Set<ID> idsCopy = Sets.newHashSet(ids); if (idsCopy.size() <= OracleConstants.MAX_IN_COUNT) { return super.findAllById(ids); } return findAll(idsCopy); } private List<T> findAll(Collection<ID> ids) { List<List<ID>> idChunks = Lists.partition(new ArrayList<>(ids), 1000); return idChunks .stream() .map(this::findAllById) .flatMap(List::stream) .collect(Collectors.toList()); }
أصبح الأمر أفضل: أولاً ، قمنا بتنظيف رمز العمل من طبقات البنية التحتية ، وثانياً ، وسعنا نطاق حلنا ليشمل جميع مستودعات المشروع التي تمتد BaseJpaRepository
مستودعات BaseJpaRepository
. هناك أيضا عيوب. العنصر الرئيسي هو عدة طلبات بدلاً من طلب واحد ، وكذلك (ينبع من الطلب الرئيسي) - الحاجة إلى تصفية المفاتيح ، لأنه إذا لم يتم ذلك ، فقد يظهر نفس المفتاح في idChunks
مختلفة. هذا ، بدوره ، يعني أنه سيتم إدراج نفس الكيان في القائمة مرتين ، وبالتالي ، ستتم معالجته مرتين. لا نحتاج إلى ذلك ، لذلك يوجد حل آخر أكثر تطوراً:
@Override public List<T> findAllById(Iterable<ID> ids) { Assert.notNull(ids, "The given Iterable of Id's must not be null!"); ArrayList<ID> idsCopy = Lists.newArrayList(ids); if (idsCopy.size() <= OracleConstants.MAX_IN_COUNT) { return super.findAllById(ids); } return findAll(idsCopy); } private List<T> findAll(ArrayList<ID> ids) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<T> query = cb.createQuery(getDomainClass()); Root<T> from = query.from(getDomainClass()); Predicate predicate = toPredicate(cb, ids, from); query = query.select(from).where(predicate); return entityManager.createQuery(query).getResultList(); } private Predicate toPredicate(CriteriaBuilder cb, ArrayList<ID> ids, Root<T> root) { List<List<ID>> chunks = Lists.partition(ids, OracleConstants.MAX_IN_COUNT); SingularAttribute<? super T, ?> id = entityInfo.getIdAttribute(); Predicate[] predicates = chunks.stream() .map(chunk -> root.get(id).in(chunk)) .toArray(Predicate[]::new); return cb.or(predicates); }
يستخدم API Criteria ، مما يجعل من الممكن بناء استعلام نهائي واحد من النموذج
select p.* from Pupil p where p.id in (1, 2, ... , 1000) or p.id in (1001, ... , 2000) or p.id in (2001, ... , 3000)
هناك دقة: يمكن تنفيذ طلب مماثل لفترة أطول من المعتاد بسبب الظروف المرهقة ، لذلك قد تكون الطريقة الأولى (في بعض الأحيان) هي الأفضل.
هذا كل شيء ، آمل أن تكون الأمثلة مفيدة لك ومفيدة في التنمية اليومية. التعليقات والإضافات هي موضع ترحيب.