web-dev-qa-db-ja.com

空のリストをパラメーターとしてJPAクエリに渡すとエラーがスローされる

JPAクエリに空のリストを渡すと、エラーが発生します。例えば:

List<Municipality> municipalities = myDao.findAll();  // returns empty list
em.createQuery("SELECT p FROM Profile p JOIN p.municipality m WHERE m IN (:municipalities)")
    .setParameter("municipalities", municipalities)
    .getResultList();

リストが空であるため、HibernateはこれをSQLで "IN()"として生成します。これにより、Hypersonicデータベースでエラーが発生します。

Hibernateの問題追跡 にこのためのチケットがありますが、そこには多くのコメント/アクティビティはありません。他のORM製品やJPA仕様のサポートについても知りません。

毎回手動でnullオブジェクトと空のリストをチェックする必要があるという考えは好きではありません。これに対する一般的に知られているアプローチ/拡張はありますか?これらの状況にどのように対処しますか?

30
Tuukka Mustonen

JPA 1.0仕様のセクション4.6.8 In Expressionsによれば、

IN式の値のセットを定義するコンマ区切りリストには、少なくとも1つの要素が必要です。

つまり、クエリを解析してIN()を渡すHibernateの機能に関係なく、特定のデータベースによるこの構文のサポートに関係なく(PosgreSQLはJiraの問題に従っていない)、コードを移植可能にする場合は、ここで動的クエリを使用します(通常、動的クエリにはCriteria APIを使用することを好みます)。

36
Pascal Thivent

解決:

if (municipalities==null || municipalities.isEmpty())
    .setParameter("municipalities", "''")
else
    .setParameter("municipalities", municipalities)
1
Samanta

SQLクエリが次のようであると仮定

(COALESCE(:placeHolderName,NULL) IS NULL OR Column_Name in (:placeHolderName))

リストがString型の場合、次のように渡すことができます

query.setParameterList("placeHolderName", 
!CollectionUtils.isEmpty(list)? list : new ArrayList<String>(Arrays.asList("")).

また、リストに整数値が含まれている場合、構文は次のようになります。

If(!CollectionUtils.isEmpty(list)){
query.setParameterList("placeHolderName",list)
}else{
query.setParameter("placeHolderName",null, Hibernate.INTEGER)
}

1
r_divyas

返信としての実際の解決策がない後、私はこれらの状況を処理するプロキシクラスを作成しました。このアイデアは、可能な場合はネイティブ構文を保持することです。

警告:これは進行中の作業であり、非常に危険なアプローチです。以下のコードは決して完全なソリューションを意味するものではなく、何十億ものバグと恐ろしいケースが含まれている可能性があります。

つまり、BlankAwareQueryクラスはjavax.persistenceクエリをラップし、EntityManagerおよびコアクエリ文字列(空のリストや列挙のリストを含めることはできません)で初期化されます。

BlankAwareQuery query = new BlankAwareQuery(em, "SELECT p FROM Profile p");

クラスの作成後、動的パーツは

query.from("p.address a");
query.where("a IN (:addresses)");

パラメータは常に挿入されます:

query.setParameter("addresses", addresses);

ここでのポイントは、クラスがこれら(それらのfrom-partも同様)を空のリストの場合はクエリから削除し、列挙のリストの場合はそれらを操作することです。

次に電話してください:

query.getResultList();

したがって、たとえば:

List<Profile> profiles = new BlankAwareQuery(em, "SELECT p FROM Profile p")
    .from("p.address a JOIN a.municipality m").where("m IN (:municipalities)")
    .where("p.gender IN (:genders)")
    .where("p.yearOfBirth > :minYear")
    .where("p.yearOfBirth < :maxYear")
    .from("p.platforms f").where("f IN (:platforms)")
    .setParameter("municipalities", municipalities)
    .setParameter("genders", genders)
    .setParameter("minYear", minYear)
    .setParameter("maxYear", maxYear)
    .setParameter("platforms", platforms)
    .getResultList();

実際のコード(コードでは、@ Dataおよび@NonNullアノテーションにLombokを使用し、StringUtilsにApache commons langを使用しています):

public class BlankAwareQuery {

    private @Data class Parameter {
        private @NonNull String fieldName;
        private @NonNull Object value;
    }

    private @Data class ClausePair {
        private @NonNull String from;
        private @NonNull String where;
    }

    private EntityManager em;

    private List<String> select = Lists.newArrayList();
    private List<ClausePair> whereFrom = Lists.newArrayList();
    private String from;
    private List<Parameter> parameters = Lists.newArrayList();
    Query query;

    public BlankAwareQuery(EntityManager em, String query) {

        this.em = em;

        /** Select **/
        int selectStart = StringUtils.indexOf(query, "SELECT ") + 7;
        int selectEnd = StringUtils.indexOf(query, " FROM ");
        select(StringUtils.substring(query, selectStart, selectEnd));

        /** From **/
        int fromStart = selectEnd + 6;
        int fromEnd = StringUtils.indexOf(query, " WHERE ");
        if (fromEnd == -1) fromEnd = query.length();
        from(StringUtils.substring(query, fromStart, fromEnd));

        /** Where **/
        String where = "";
        if (StringUtils.contains(query, " WHERE ")) {
            where = StringUtils.substring(query, fromEnd + 7);
        }
        where(where);
    }

    private BlankAwareQuery select(String s) {
        select.add(s);
        return this;
    }

    public BlankAwareQuery from(String s) {
        from = s;
        return this;
    }

    public BlankAwareQuery where(String s) {
        ClausePair p = new ClausePair(from, s);
        whereFrom.add(p);
        from = "";
        return this;
    }

    public BlankAwareQuery setParameter(String fieldName, Object value) {

        /** Non-empty collection -> include **/
        if (value != null && value instanceof List<?> && !((List<?>) value).isEmpty()) {

            /** List of enums -> parse open (JPA doesn't support defining list of enums as in (:blaa) **/
            if (((List<?>) value).get(0) instanceof Enum<?>) {

                List<String> fields = Lists.newArrayList();

                /** Split parameters into individual entries **/
                int i = 0;
                for (Enum<?> g : (List<Enum<?>>) value) {
                    String fieldSingular = StringUtils.substring(fieldName, 0, fieldName.length() - 1) + i;
                    fields.add(":" + fieldSingular);
                    parameters.add(new Parameter(fieldSingular, g));
                    i++;
                }

                /** Split :enums into (:enum1, :enum2, :enum3) strings **/
                for (ClausePair p : whereFrom) {
                    if (p.getWhere().contains(":" + fieldName)) {
                        int start = StringUtils.indexOf(p.getWhere(), ":" + fieldName);
                        int end = StringUtils.indexOfAny(StringUtils.substring(p.getWhere(), start + 1), new char[] {')', ' '});
                        String newWhere = StringUtils.substring(p.getWhere(), 0, start) + StringUtils.join(fields, ", ") + StringUtils.substring(p.getWhere(), end + start + 1);
                        p.setWhere(newWhere);
                    }
                }
            }
            /** Normal type which doesn't require customization, just add it **/ 
            else {
                parameters.add(new Parameter(fieldName, value));
            }
        }

        /** Not to be included -> remove from and where pair from query **/
        else {
            for (Iterator<ClausePair> it = whereFrom.iterator(); it.hasNext();) {
                ClausePair p = it.next();
                if (StringUtils.contains(p.getWhere(), fieldName)) {
                    it.remove();
                }
            }
        }

        return this;
    }

    private String buildQueryString() {

        List<String> from = Lists.newArrayList();
        List<String> where = Lists.newArrayList();

        for (ClausePair p : whereFrom) {
            if (!p.getFrom().equals("")) from.add(p.getFrom());
            if (!p.getWhere().equals("")) where.add(p.getWhere());
        }

        String selectQuery = StringUtils.join(select, ", ");
        String fromQuery = StringUtils.join(from, " JOIN ");
        String whereQuery = StringUtils.join(where, " AND ");

        String query = "SELECT " + selectQuery + " FROM " + fromQuery + (whereQuery == "" ? "" : " WHERE " + whereQuery);

        return query;
    }

    public Query getQuery() {
        query = em.createQuery(buildQueryString());
        setParameters();
        return query;
    }

    private void setParameters() {
        for (Parameter par : parameters) {
            query.setParameter(par.getFieldName(), par.getValue());
        }
    }

    public List getResultList() {
        return getQuery().getResultList();
    }

    public Object getSingleResult() {
        return getQuery().getSingleResult();
    }
}
1
Tuukka Mustonen

通常1から始まるDBシーケンスIDをクエリしているため、リストに0を追加できます

if (excludeIds.isEmpty()) {
    excludeIds.add(new Long("0"));
}
List<SomeEntity> retval = someEntityRepo.findByIdNotIn(excludeIds);

多分-1でも動作します。 jpa reposを使用するための小さな回避策。

0
user10747457