Spring Data JPA: Dateien bringen

Grüße, dies ist der zweite Beitrag über Spring Data JPA. Der erste Teil war ausschließlich Unterwasserrechen sowie Tipps von erfahrenen gewidmet. In diesem Teil werden wir darüber sprechen, wie Sie das Framework an Ihre Bedürfnisse anpassen können. Alle beschriebenen Beispiele finden Sie hier .


Zählt


Beginnen wir vielleicht mit einer einfachen und gleichzeitig gemeinsamen Aufgabe: Beim Laden einer Entität muss ihre „Tochter“ selektiv heruntergeladen werden. Betrachten Sie ein einfaches Beispiel:


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

Die untergeordnete Entität in unserem Beispiel ist faul: Wir möchten beim Empfang von untergeordneten Elementen keine unnötigen Daten laden (und eine andere Tabelle in der SQL-Abfrage verknüpfen). In einigen Fällen wissen wir in unserer Bewerbung jedoch mit Sicherheit, dass wir sowohl das Kind als auch seine Eltern brauchen werden. Wenn Sie die Entität faul lassen, erhalten wir 2 separate Anfragen. Wenn Sie durch Entfernen von FetchType.LAZY schnelles Laden FetchType.LAZY , werden beide Entitäten immer bei der ersten Anforderung geladen (und wir möchten dies nicht).


JPQL bietet eine sofort einsatzbereite Lösung - dies ist das Schlüsselwort 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); } 

Diese Anfrage ist einfach und klar, hat aber Nachteile:


  • Wir haben die Logik von JpaRepository::findById tatsächlich dupliziert, indem wir explizites Laden hinzugefügt haben
  • Jede mit @Query beschriebene @Query beim Start der Anwendung überprüft. @Query müssen die Abfrage analysiert, Argumente usw. überprüft werden (siehe org.springframework.data.jpa.repository.query.SimpleJpaQuery :: validateQuery ). All dies ist Arbeit, die Zeit und Gedächtnis braucht.
  • Die Verwendung eines solchen Ansatzes in einem großen Projekt mit Dutzenden von Repositories und verschlungenen Entitäten (manchmal mit einem Dutzend "Töchtern") wird zu einer kombinatorischen Explosion führen.

Zählungen helfen uns:


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

Das Diagramm selbst ist leicht zu beschreiben, Schwierigkeiten beginnen bei der Verwendung. Spring Data JPA auf seiner Seite schlägt vor, dies folgendermaßen zu tun (wie in unserem Fall):


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

Hier sehen wir alle die gleichen Probleme (außer dass die schriftliche Anfrage etwas einfacher geworden ist). Sie können sie mit Hilfe der Feinabstimmung auf einen Schlag beenden. Erstellen Sie Ihre eigene Schnittstelle, über die wir anstelle des JpaRepository Repositorys JpaRepository :


 @NoRepositoryBean public interface BaseJpaRepository<T, ID extends Serializable> extends JpaRepository<T, ID> { T findById(ID id, String graphName); } 

Jetzt Implementierung:


 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!"); //  EntityGraph<?> graph = entityManager.getEntityGraph(graphName); Map<String, Object> hints = singletonMap(QueryHints.HINT_LOADGRAPH, graph); return entityManager.find(getDomainClass(), id, hints); } 

BaseJpaRepositoryImpl Spring nun, BaseJpaRepositoryImpl als Grundlage für alle Repositorys unserer Anwendung zu verwenden:


 @EnableJpaRepositories(repositoryBaseClass = BaseJpaRepositoryImpl.class) public class AppConfig { } 

Jetzt ist unsere Methode in allen Repositorys verfügbar, die von unserem BaseJpaRepository geerbt BaseJpaRepository .


Dieser Ansatz hat einen Nachteil, der ein sehr fettes Schwein setzen kann.


Versuche selbst zu denken

Das Problem ist, dass der Ruhezustand (zumindest zum Zeitpunkt des Schreibens) nicht mit den Namen der Diagramme und den Diagrammen selbst übereinstimmt. Aus diesem Grund kann es zu einem Laufzeitfehler kommen, wenn wir so etwas ausführen


 Optional<MyEntity> entity = repository.findById(id, NON_EXISTING_GRAPH); 

Sie können den Zustand der Lösung mithilfe des Tests überprüfen:


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

Als die Bäume groß waren


Und wir waren klein und unerfahren, wir mussten oft diesen Code sehen:


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

Dieser Code sammelt die Anfrage Stück für Stück. Die Nachteile dieses Ansatzes liegen auf der Hand:


  • du musst viel mit deinen Händen machen
  • Wo manuelle Arbeit ist, gibt es Fehler
  • Keine Syntaxhervorhebung (Tippfehler werden zur Laufzeit angezeigt)
  • Es ist sehr schwierig, den Code zu erweitern und zu pflegen

Wenig später erschien die Kriterien-API, mit der wir den obigen Code ein wenig komprimieren konnten:


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

Die Verwendung von Kriterien hat mehrere Vorteile:


  • die Möglichkeit, ein Metamodell anstelle von "verdrahteten" Werten wie "Datum" zu verwenden
  • Einige Fehler beim Erstellen der Anforderung stellen sich als Kompilierungsfehler heraus, d. h. sie werden bereits beim Schreiben erkannt
  • Der Code ist kürzer und verständlicher als beim blöden Einfügen von Strings

Es gibt auch Nachteile:


  • Der Code ist kompliziert genug, um zu verstehen
  • Um zu lernen, wie man solche Abfragen schreibt, müssen Sie Ihre Hand füllen (ich erinnere mich an den wildesten Schmerz, als ich mich zum ersten Mal mit der Fehlerkorrektur in solchen Abfragen befassen musste, die manchmal aus 100-150 Zeilen bestehen, mit Verzweigung usw.)
  • Eine komplexe Abfrage ist ziemlich umständlich (50 Zeilen sind weit vom Limit entfernt).

Ich möchte es einfach und mit Vergnügen entwickeln, daher mag ich diese beiden Methoden nicht.


Wenden wir uns der bereits von uns untersuchten Entität zu:


 @Entity public class Child { @Id private Long id; @JoinColumn(name = "parent_id") @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Parent parent; //... @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL) @LazyCollection(value = LazyCollectionOption.EXTRA) private List<Toy> toys = new ArrayList<>(); } 

Ich möchte in der Lage sein, eine Entität in verschiedenen Modi (und deren Kombinationen) zu laden:


  • Eltern laden (oder nicht)
  • Spielzeug laden (oder nicht)
  • Kinder nach Alter sortieren

Wenn Sie dieses Problem direkt lösen, d. H. Indem Sie viele Abfragen schreiben, die dem ausgewählten Lademodus entsprechen, führt dies sehr schnell zu einer kombinatorischen Explosion:


  @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(); //... 

Es gibt eine einfache und elegante Möglichkeit, dieses Problem zu lösen: eine Kombination aus SQL / HQL- und Template-Engines. "Freemarker" wurde für meine Projekte verwendet, obwohl andere Lösungen verwendet werden können ("Timlif", "Mustash" usw.).


Beginnen wir mit dem Erstellen. Zunächst müssen wir die Abfrage in einer Datei beschreiben, die die Erweiterung *.hql.ftl oder *.sql.ftl (wenn "reines" SQL verwendet wird):


 #* @vtlvariable name="fetchParent" type="java.lang.Boolean" *# #* @vtlvariable name="fetchToys" type="java.lang.Boolean" *# #* @vtlvariable name="orderByAge" type="java.lang.Boolean" *# select child from Child child #if($fetchParent) left join fetch child.parent #end #if($fetchToys) left join fetch child.toys #end #if($orderByAge) order by child.age #end 

Jetzt brauchen Sie einen Handler:


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

Nichts kompliziertes. Zum Repository gelangen. Offensichtlich JpaRepository die Schnittstelle, die JpaRepository erbt, nicht zu uns. Stattdessen nutzen wir die Gelegenheit, um eigene Repositorys zu erstellen:


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

Um die findUsingTemplate Methode über findUsingTemplate zugänglich zu ChildRepository Sie ChildRepository tun:


 public interface ChildRepository extends BaseJpaRepository<Child, Long>, ChildRepositoryCustom { //... } 

Ein wichtiges Merkmal, das mit dem Namen verbunden ist

Spring bindet unsere Klasse und Schnittstellen nur mit dem richtigen Namen zusammen:


  • Childrepository
  • ChildRepository Custom
  • ChildRepository Impl

Denken Sie daran, da im Falle eines Namensfehlers eine unverständliche Ausnahme ausgelöst wird, anhand derer die Fehlerursache nicht zu verstehen ist.


Mit diesem Ansatz können Sie jetzt komplexere Probleme lösen. Angenommen, wir müssen eine Auswahl basierend auf vom Benutzer ausgewählten Merkmalen treffen. Mit anderen Worten, wenn der Benutzer die Daten "von" und "bis" nicht angegeben hat, erfolgt keine Zeitfilterung. Wenn nur das Datum "von" oder nur das Datum "bis" angegeben wird, erfolgt die Filterung in eine Richtung. Wenn beide Daten angegeben sind, fallen nur Datensätze zwischen den angegebenen Daten in die Auswahl:


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

Nun die Vorlage:


 <#-- @ftlvariable name="request" type="...RequestDto" --> select child from Child child <#if request.hasDateFrom() && request.hasDateTo()> where child.birthDate >= :dateFrom and child.birthDate <= :dateTo <#elseif request.hasDateFrom()> where child.birthDate >= :dateFrom <#elseif request.hasDateTo()> where child.birthDate <= :dateTo </#if> 

Oracle und nvl


Betrachten Sie die Essenz:


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

Diese Entität wird in der Abfrage verwendet (DBMS, wie wir uns erinnern, haben wir Oracle):


 @Query("select nvl(record.fxRate, record.avgRate) " + " from DailyRecord record " + "where record.currency = :currency") BigDecimal findRateByCurrency(@Param("currency") String currency); 

Dies ist eine funktionierende, gültige Anfrage. Es gibt jedoch ein kleines Problem, auf das SQL-Experten wahrscheinlich hinweisen werden. Tatsache ist, dass nvl in Oracle nicht faul ist. Mit anderen Worten, wenn wir die findRateByCurrency Methode findRateByCurrency , wird das 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 = ? 

Selbst wenn der Wert dr.fixed_rate vorhanden ist, berechnet das DBMS den Wert, der vom zweiten Ausdruck in nvl , in unserem Fall


 select avg(r.record_rate) from daily_record r where r.currency = dr.currency) 

Der Leser weiß wahrscheinlich bereits, wie er dem unnötigen Gewicht der Anfrage ausweichen kann: Dies ist natürlich das coalesce Schlüsselwort, das im Vergleich zu nvl seiner Faulheit und der Fähigkeit, mehr als zwei Ausdrücke zu akzeptieren, günstig ist. Korrigieren wir unsere Anfrage:


 @Query("select coalesce(record.fxRate, record.avgRate) " + " from DailyRecord record " + "where record.currency = :currency") BigDecimal findRateByCurrency(@Param("currency") String currency); 

Und dann, wie sie sagen, plötzlich:


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

Die Anfrage blieb gleich. Das liegt daran, dass der Orakel-Dialekt aus der Box die nvl in eine nvl Kette nvl .


Bemerkung

Wenn Sie dieses Verhalten reproduzieren möchten, löschen Sie die zweite Zeile im Konstruktor der CustomOracleDialect- Klasse und führen Sie den DailyRecordRepositoryTest::findRateByCurrency


Um dem auszuweichen, müssen Sie Ihren eigenen Dialekt erstellen und in der Anwendung verwenden:


 public class CustomOracleDialect extends Oracle12cDialect { public CustomOracleDialect() { super(); registerFunction("coalesce", new StandardSQLFunction("coalesce")); } } 

Ja, das ist so einfach. Jetzt binden wir den erstellten Dialekt an die Anwendung:


 spring: jpa: database-platform: com.luxoft.logeek.config.CustomOracleDialect 

Ein anderer (veralteter) Weg:
 spring: jpa: properties: hibernate.dialect: com.luxoft.logeek.config.CustomOracleDialect 

Die wiederholte Ausführung der Anfrage ergibt den begehrten 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 = ? 

Oracle- und Seitenanforderungen


Im Allgemeinen bietet die Vervollständigung eines Dialekts umfangreiche Möglichkeiten zur Manipulation von Abfragen. Bei der Entwicklung einer Anwendung und eines Webface tritt häufig die Aufgabe des seitenweisen Hochladens von Daten auf. Mit anderen Worten, wir haben mehrere hunderttausend Datensätze in der Datenbank, die jedoch in Paketen mit 10/50/100 Datensätzen pro Seite angezeigt werden. Das sofort einsatzbereite Frühlingsdatum bietet dem Entwickler ähnliche Funktionen:


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

Dieser Ansatz hat einen erheblichen Nachteil, nämlich die Ausführung von zwei Abfragen, von denen die erste die Daten abruft und die zweite ihre Gesamtzahl in der Datenbank bestimmt (dies ist erforderlich, um die Gesamtdatenmenge im Page Objekt anzuzeigen). In unserem Fall gibt ein Aufruf dieser Methode die folgenden Anforderungen (Protokollierung mit 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 = '' 

Wenn die Abfrage sehr umfangreich ist (viele Tabellen werden durch eine nicht indizierbare Spalte, nur viele Verknüpfungen, eine schwierige Auswahlbedingung usw. verbunden), kann dies zu einem Problem werden. Aber da wir Oracle haben, können Sie mit der Rownum-Pseudospalte mit einer Anfrage auskommen.


Dazu müssen wir unseren Dialekt beenden und die Funktion beschreiben, mit der alle Datensätze gezählt werden:


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

Schreiben Sie nun eine neue Abfrage (in der ChildRepositoryImpl Klasse):


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

Beim Aufrufen dieses Codes wird eine Anforderung ausgeführt


 select * from (select c.id, c.age, count(c.id) over () -- <----- from child c inner join parent p on c.parent_id = p.id where p.name = '') where rownum <= 3 

Mit dem Ausdruck count(c.id) over () Sie die Gesamtdatenmenge count(c.id) over () und von der Datenklasse PageImpl um sie an den PageImpl Konstruktor zu übergeben. Es gibt eine Möglichkeit, dies eleganter zu gestalten, ohne der Datenklasse ein weiteres Feld hinzuzufügen. Betrachten Sie es als Hausaufgabe :) Sie können die Lösung mit dem ProjectionVsDataTest- Test testen.


Fallstricke der Anpassung


Wir haben ein cooles Projekt mit Oracle und Spring Date. Unsere Aufgabe ist es, die Leistung eines solchen Codes zu verbessern:


 List<Long> ids = getIds(); ids.stream() .map(repository::findById) .filter(Optional::isPresent) .map(Optional::get) .forEach(this::sendToSchool); 

Der Nachteil liegt an der Oberfläche: Die Anzahl der Datenbankabfragen entspricht der Anzahl der eindeutigen Schlüssel. Es gibt eine bekannte Methode, um diese Schwierigkeit zu überwinden:


 List<Long> ids = getIds(); repository .findAllById(ids) .forEach(this::sendToSchool); 

Der Vorteil der Mehrfachabtastung liegt auf der Hand: Wenn wir früher viele ähnliche Abfragen des Formulars hatten


 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 

dann sind sie jetzt zu einem zusammengebrochen


 select p.* from Pupil p where p.id in (1, 2, 3, ... ) 

Es scheint einfacher zu sein und wurde gut. Das Projekt wächst, entwickelt sich, die Daten vervielfachen sich und sobald das Unvermeidliche eintritt:


Aki donnert am klaren Himmel
 ERROR - ORA-01795: maximum number of expressions in a list is 1000 

Wir müssen wieder nach einem Ausweg suchen (nicht zur alten Version zurückkehren). Da Oracle nicht zulässt, dass er mehr als 1000 Schlüssel füttert, können Sie den gesamten Datensatz in gleiche Anteile von nicht mehr als 1000 aufteilen und mehrere Abfragen ausführen:


 List<List<Long>> idChunks = cgccLists.partition(ids, 1000); //* idChunks.forEach(idChunk -> repository.findAllById(idChunk).forEach(this::sendToSchool) ); //* cgccLists = com.google.common.collect.Lists 

Diese Methode funktioniert, riecht aber leicht (oder?): Wenn Sie an anderen Stellen auf solche Schwierigkeiten stoßen, müssen Sie denselben Garten umzäunen. Versuchen wir, das Problem eleganter zu lösen, indem BaseJpaRepositoryImpl . Der einfachste Weg, dies zu tun, besteht darin, die obige Logik nach innen zu übertragen und sie vor dem Benutzer zu verbergen:


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

Es wurde besser: Erstens haben wir den BaseJpaRepository von den Infrastrukturebenen entfernt und zweitens haben wir den Umfang unserer Lösung auf alle Projekt-Repositorys erweitert, die BaseJpaRepository . Es gibt auch Nachteile. Die Hauptanforderung besteht aus mehreren Anforderungen anstelle einer und auch (ergibt sich aus der Hauptanforderung) aus der Notwendigkeit, die Schlüssel zu filtern. idChunks kann derselbe Schlüssel in verschiedenen idChunks . Dies bedeutet wiederum, dass dieselbe Entität zweimal in die Liste aufgenommen und dementsprechend zweimal verarbeitet wird. Wir brauchen das nicht, deshalb hier eine andere, komplexere Lösung:


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

Es verwendet die Kriterien-API, mit der eine letzte Abfrage des Formulars erstellt werden kann


 select p.* from Pupil p where p.id in (1, 2, ... , 1000) or p.id in (1001, ... , 2000) or p.id in (2001, ... , 3000) 

Es gibt eine Subtilität: Eine ähnliche Anforderung kann aufgrund der umständlichen Bedingungen länger als gewöhnlich ausgeführt werden, so dass die erste Methode (manchmal) vorzuziehen ist.


Das ist alles, ich hoffe, die Beispiele waren nützlich für Sie und nützlich für die tägliche Entwicklung. Kommentare und Ergänzungen sind willkommen.

Source: https://habr.com/ru/post/de444240/


All Articles