~/home of geeks

JPA Criteria Join Handling

· 1630 Wörter · 8 Minute(n) Lesedauer

hall full of ice figures, sci-fi

Meines Erachtens ist die JPA Criteria API nicht ganz so schön gelungen. Um Typesafe zu sein, hat man sehr unleserliche Konstrukte zu bauen. Ich habe mich mit dem Thema Joins in der JPA Criteria API beschäftigt.

Wenn man Joins in den Abfragen hat, die von verschiedenen Codeteilen generiert werden, kommt man früher oder später nicht umher, die Joins so zu verwalten, dass man bereits gemachte Joins recyceln kann.

Beispiel: Navigation von Tabelle A nach Tabelle B und Selektion der Spalte o

SELECT a FROM A AS a JOIN B AS b ON a.b_id = b.id WHERE b.o = 'x';

In JPA Criteria API formuliert wäre dies äquivalent zu:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<BeanA> query = builder.createQuery(BeanA.class);
Root<BeanA> root = query.from(domainClass);
Join<BeanA, BeanB> join = root.join(BeanA_.myBeanB);
Predicate condition = builder.equal(join.get(BeanB_.o), builder.literal("x"));
TypedQuery<BeanA> typedQuery = entityManager.createQuery(query.select(root).where(condition));
// ...

Möchte man nun an einer anderen Codestelle, welche die obere Stelle nicht im Zugriff hat, das weitere Attribut p von Tabelle B benutzen, wird man geneigt sein, den gleichen Join zu definieren:

Join<BeanA, BeanB> join = root.join(BeanA_.myBeanB);
Predicate condition = builder.equal(join.get(BeanB_.p), builder.literal("y"));

Der erneute Aufruf root.join() erzeugt aber einen neuen, ganz eigenen Join:

SELECT a FROM A AS a
  JOIN B AS b ON a.b_id = b.id
  JOIN B AS b_2 ON a.b_id = b_2.id
  WHERE b.o = 'x' AND b_2.p= 'y';

Möchte man also einen weiteren Join verhindern und das vorhandene Join wiederverwerten, muss man sich die an den Joins beteiligten Objekte genauer anschauen. Das Interface javax.persistence.criteria.From hat die Methode getJoins(), welche uns alle Joins an dem Objekt (samt einiger Metainformationen) liefert. Wenn man nun diese Joins per alias() mit Standardnamen versieht, kann man die entsprechenden Joins wiederfinden und -benutzen.

Hierzu wird für jeden Join entweder ein angegebener Alias oder ein Standardalias erzeugt. Der Standardalias setzt sich aus dem voll qualifizierten Java Klassennamen des Quellobjektes (from), dem Jointypen (inner, right, left) und dem Attributnamen des Joinziels zusammen. So wird aus einem Inner-Join von org.mypackage.MyBean auf MyBean.myChildObject der Alias org_mypackage_MyBean_inner_myChildObject. Die Angabe eines eigenen Alias erlaubt es bei Bedarf neue Joins anzulegen, auch wenn es zu einer Assoziation schon einen Join gibt.

import org.apache.commons.lang.StringUtils;

import javax.persistence.criteria.From;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.ListJoin;
import javax.persistence.criteria.SetJoin;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.ListAttribute;
import javax.persistence.metamodel.SetAttribute;
import javax.persistence.metamodel.SingularAttribute;
import java.util.Collections;
import java.util.Set;

/**
 * Hilfsklasse für JPA Criteria API Queries, die Joins wiederverwerten wollen.
 * Der Standardalias setzt sich aus dem voll qualifizierten Java Klassennamen des Quellobjektes (from),
 * dem Jointypen (inner, right, left) und dem Attributnamen des Joinziels zusammen.
 *
 * @author Serhat Cinar
 */
public class JPACriteriaJoinHandler {
    /**
     * Default Join-Typ, wenn kein Join-Typ angegeben wird.
     */
    public static final JoinType DEFAULT_JOIN_TYPE = JoinType.INNER;

    /**
     * Erlaubt die Wiederbenutzung von Joins.
     * Benutzt {@link #DEFAULT_JOIN_TYPE} als {@link JoinType}.
     *
     * @param from
     * @param attribute
     * @param <SOURCE>
     * @param <ELEMENT>
     * @return
     */
    public <SOURCE, ELEMENT> ListJoin<SOURCE, ELEMENT> join(From<?, SOURCE> from, ListAttribute<SOURCE, ELEMENT> attribute) {
        return join(from, attribute, (JoinType) null);
    }

    /**
     * Erlaubt die Wiederbenutzung von Joins.
     * Benutzt {@link #DEFAULT_JOIN_TYPE} als {@link JoinType}.
     *
     * @param from
     * @param attribute
     * @param <SOURCE>
     * @param <ELEMENT>
     * @param joinAlias
     * @return
     */
    public <SOURCE, ELEMENT> ListJoin<SOURCE, ELEMENT> join(From<?, SOURCE> from, ListAttribute<SOURCE, ELEMENT> attribute, String joinAlias) {
        return join(from, attribute, null, joinAlias);
    }

    /**
     * Erlaubt die Wiederbenutzung von Joins.
     *
     * @param from
     * @param attribute
     * @param joinType
     * @param <SOURCE>
     * @param <ELEMENT>
     * @return
     */
    public <SOURCE, ELEMENT> ListJoin<SOURCE, ELEMENT> join(From<?, SOURCE> from, ListAttribute<SOURCE, ELEMENT> attribute, JoinType joinType) {
        return join(from, attribute, joinType, null);
    }

    /**
     * Erlaubt die Wiederbenutzung von Joins.
     *
     * @param from
     * @param attribute
     * @param joinType
     * @param <SOURCE>
     * @param <ELEMENT>
     * @param joinAlias
     * @return
     */
    public <SOURCE, ELEMENT> ListJoin<SOURCE, ELEMENT> join(From<?, SOURCE> from, ListAttribute<SOURCE, ELEMENT> attribute, JoinType joinType, String joinAlias) {
        if (joinType == null) {
            joinType = DEFAULT_JOIN_TYPE;
        }
        if (StringUtils.isBlank(joinAlias)) {
            joinAlias = getDefaultJoinAlias(from, attribute, joinType);
        }
        joinAlias = escapeJoinAlias(joinAlias);
        ListJoin<SOURCE, ELEMENT> join = (ListJoin<SOURCE, ELEMENT>) getJoin(from, attribute, joinType, joinAlias);
        if (join == null) {
            join = from.join(attribute, joinType);
            join.alias(joinAlias);
        }
        return join;
    }

    /**
     * Erlaubt die Wiederbenutzung von Joins.
     * Benutzt {@link #DEFAULT_JOIN_TYPE} als {@link JoinType}.
     *
     * @param from
     * @param attribute
     * @param <SOURCE>
     * @param <ELEMENT>
     * @return
     */
    public <SOURCE, ELEMENT> SetJoin<SOURCE, ELEMENT> join(From<?, SOURCE> from, SetAttribute<SOURCE, ELEMENT> attribute) {
        return join(from, attribute, (JoinType) null);
    }


    /**
     * Erlaubt die Wiederbenutzung von Joins.
     * Benutzt {@link #DEFAULT_JOIN_TYPE} als {@link JoinType}.
     *
     * @param from
     * @param attribute
     * @param <SOURCE>
     * @param <ELEMENT>
     * @param joinAlias
     * @return
     */
    public <SOURCE, ELEMENT> SetJoin<SOURCE, ELEMENT> join(From<?, SOURCE> from, SetAttribute<SOURCE, ELEMENT> attribute, String joinAlias) {
        return join(from, attribute, (JoinType) null, joinAlias);
    }

    /**
     * Erlaubt die Wiederbenutzung von Joins.
     *
     * @param from
     * @param attribute
     * @param joinType
     * @param <SOURCE>
     * @param <ELEMENT>
     * @return
     */
    public <SOURCE, ELEMENT> SetJoin<SOURCE, ELEMENT> join(From<?, SOURCE> from, SetAttribute<SOURCE, ELEMENT> attribute, JoinType joinType) {
        return join(from, attribute, joinType, null);
    }

    /**
     * Erlaubt die Wiederbenutzung von Joins.
     *
     * @param from
     * @param attribute
     * @param joinType
     * @param joinAlias
     * @param <SOURCE>
     * @param <ELEMENT>
     * @return
     */
    public <SOURCE, ELEMENT> SetJoin<SOURCE, ELEMENT> join(From<?, SOURCE> from, SetAttribute<SOURCE, ELEMENT> attribute, JoinType joinType, String joinAlias) {
        if (joinType == null) {
            joinType = DEFAULT_JOIN_TYPE;
        }
        if (StringUtils.isBlank(joinAlias)) {
            joinAlias = getDefaultJoinAlias(from, attribute, joinType);
        }
        joinAlias = escapeJoinAlias(joinAlias);
        SetJoin<SOURCE, ELEMENT> join = (SetJoin<SOURCE, ELEMENT>) getJoin(from, attribute, joinType, joinAlias);
        if (join == null) {
            join = from.join(attribute, joinType);
            join.alias(joinAlias);
        }
        return join;
    }


    /**
     * Erlaubt die Wiederbenutzung von Joins.
     * Benutzt {@link #DEFAULT_JOIN_TYPE} als {@link JoinType}.
     *
     * @param from
     * @param attribute
     * @param <SOURCE>
     * @param <ELEMENT>
     * @return
     */
    public <SOURCE, ELEMENT> Join<SOURCE, ELEMENT> join(From<?, SOURCE> from, SingularAttribute<SOURCE, ELEMENT> attribute) {
        return join(from, attribute, (JoinType) null);
    }

    /**
     * Erlaubt die Wiederbenutzung von Joins.
     * Benutzt {@link #DEFAULT_JOIN_TYPE} als {@link JoinType}.
     *
     * @param from
     * @param attribute
     * @param joinAlias
     * @param <SOURCE>
     * @param <ELEMENT>
     * @return
     */
    public <SOURCE, ELEMENT> Join<SOURCE, ELEMENT> join(From<?, SOURCE> from, SingularAttribute<SOURCE, ELEMENT> attribute, String joinAlias) {
        return join(from, attribute, null, joinAlias);
    }


    /**
     * Erlaubt die Wiederbenutzung von Joins.
     *
     * @param from
     * @param attribute
     * @param joinType
     * @param <SOURCE>
     * @param <ELEMENT>
     * @return
     */
    public <SOURCE, ELEMENT> Join<SOURCE, ELEMENT> join(From<?, SOURCE> from, SingularAttribute<SOURCE, ELEMENT> attribute, JoinType joinType) {
        return join(from, attribute, joinType, null);
    }

    /**
     * Erlaubt die Wiederbenutzung von Joins.
     *
     * @param from
     * @param attribute
     * @param joinType
     * @param joinAlias
     * @param <SOURCE>
     * @param <ELEMENT>
     * @return
     */
    public <SOURCE, ELEMENT> Join<SOURCE, ELEMENT> join(From<?, SOURCE> from, SingularAttribute<SOURCE, ELEMENT> attribute, JoinType joinType, String joinAlias) {
        if (joinType == null) {
            joinType = DEFAULT_JOIN_TYPE;
        }
        if (StringUtils.isBlank(joinAlias)) {
            joinAlias = getDefaultJoinAlias(from, attribute, joinType);
        }
        joinAlias = escapeJoinAlias(joinAlias);
        Join<SOURCE, ELEMENT> join = getJoin(from, attribute, joinType, joinAlias);
        if (join == null) {
            join = from.join(attribute, joinType);
            join.alias(joinAlias);
        }
        return join;
    }

    /**
     * Menge der erlaubten Zeichen für Join-Aliase für Lookups.<br/>
     * Der Einfachheit halber "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789".<br/>
     * Zeichen wie "." oder "-" sind nicht zugelassen.
     */
    public static final Set<Character> ALLOWED_ALIAS_SYMBOLS;
    static {
        Set<Character> tmpAllowedAliasSymbols = new HashSet<>();
        for (char c : "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789".toCharArray()) {
            tmpAllowedAliasSymbols.add(c);
        }
        ALLOWED_ALIAS_SYMBOLS = Collections.unmodifiableSet(tmpAllowedAliasSymbols);
    }

    /**
     * Escaped Join-Aliase, da diese nur bestimmte Zeichen enthalten dürfen ({@link #ALLOWED_ALIAS_SYMBOLS}).<br/>
     * Erlaubt sind US-Buchstaben (lower und uppercase), Ziffern und der Underscore.
     * Alle anderen Zeichen (auch Leerzeichen) werden zu einem Underscore umgewandelt.
     *
     * @param joinAlias
     * @return
     */
    private String escapeJoinAlias(String joinAlias) {
        final StringBuilder escapedJoinAlias = new StringBuilder();
        for (char c : joinAlias.toCharArray()) {
            if (ALLOWED_ALIAS_SYMBOLS.contains(c)) {
                escapedJoinAlias.append(c);
            } else {
                escapedJoinAlias.append("_");
            }
        }
        return escapedJoinAlias.toString();
    }

    /**
     * Erzeugt einen Aliasnamen aus der Relation.<br/>
     * Der Joinname ist üblicherweise der Voll-Qualifizierte Java Klassenname des Ursprungsobjektes (from) und der Attributname
     * des Joinziels (attribute) und dem Jointypen (joinType).<br/>
     * Beispiel:<br/>
     * Inner-Join von MessageBean auf MessageBean#controller:<br/>
     * "myproject.message.entity.MessageBean.inner.controller"
     *
     * @param from
     * @param attribute
     * @param joinType
     * @return
     */
    private String getDefaultJoinAlias(From from, Attribute attribute, JoinType joinType) {
        final StringBuilder defaultJoinAlias = new StringBuilder();
        defaultJoinAlias.append(from.getJavaType().getName());
        switch (joinType) {
            case INNER:
                defaultJoinAlias.append(".inner.");
                break;
            case RIGHT:
                defaultJoinAlias.append(".right.");
                break;
            case LEFT:
                defaultJoinAlias.append(".left.");
                break;
        }
        defaultJoinAlias.append(attribute.getName());
        return defaultJoinAlias.toString();
    }

    /**
     * Sucht am Quellobjekt der Relation (from) nach einem passenden Join mit gleichem Relationsziel (attribute),
     * gleichem Join-typen (joinType) und gleichem Alias (joinAlias).
     *
     * @param from
     * @param attribute
     * @param joinType
     * @param joinAlias
     * @param <SOURCE>
     * @param <ELEMENT>
     * @return
     */
    private <SOURCE, ELEMENT> Join<SOURCE, ELEMENT> getJoin(From<?, SOURCE> from, Attribute<SOURCE, ELEMENT> attribute, JoinType joinType, String joinAlias) {
        if (joinAlias == null) {
            joinAlias = getDefaultJoinAlias(from, attribute, joinType);
        }
        for (final Join<SOURCE, ?> join : from.getJoins()) {
            if (joinAlias.equals(join.getAlias())) {
                if (equals(join, attribute, joinType)) {
                    return (Join<SOURCE, ELEMENT>) join;
                }
            }
        }
        return null;
    }

    private boolean equals(Join joinOne, Attribute attributeTwo, JoinType joinTypeTwo) {
        return equals(joinOne.getAttribute(), attributeTwo, joinOne.getJoinType(), joinTypeTwo);
    }

    private boolean equals(Attribute attributeOne, Attribute attributeTwo, JoinType joinTypeOne, JoinType joinTypeTwo) {
        return equals(attributeOne, attributeTwo) && joinTypeOne.equals(joinTypeTwo);
    }

    /**
     * Vergleicht zwei {@link Attribute}-Objekte auf Gleichheit.
     * Hier geht es um die Relation, welche durch das Attribut abgebildet wird.
     *
     * @param attributeOne
     * @param attributeTwo
     * @return
     */
    private boolean equals(Attribute attributeOne, Attribute attributeTwo) {
        // Referenz-Identität
        if (attributeOne == attributeTwo) {
            return true;
        }
        if (attributeOne != null && attributeTwo == null) {
            return false;
        }
        // Uns nicht genauer bekannte Prüfung auf Identität
        if (attributeOne.equals(attributeTwo)) {
            return true;
        }
        if (attributeOne == null && attributeTwo != null) {
            return false;
        }

        // Prüfung auf deklarierende Klasse, eigene Klasse und Relationsname
        boolean sameClass = attributeOne.getClass().equals(attributeTwo.getClass());
        if (sameClass) {
            boolean sameDeclaringType = attributeOne.getDeclaringType().equals(attributeTwo.getDeclaringType());
            if (sameDeclaringType) {
                boolean sameJavaType = attributeOne.getJavaType().equals(attributeTwo.getJavaType());
                if (sameJavaType) {
                    boolean sameName = attributeOne.getName().equals(attributeTwo.getName());
                    if (sameName) {
                        boolean sameMemberType = attributeOne.getJavaMember().equals(attributeTwo.getJavaType());
                        if (sameMemberType) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }
}