Oracle Text Criteria Query in JPA
Asked Answered
G

3

6

Is it possible to perform a JPA Criteria Query using Oracle Text's contains statement, and if so how?

Glace answered 18/10, 2011 at 18:6 Comment(0)
R
3

Doubt it. The API is there across all RDBMS, and provides certain constructs like "LIKE"/"SUBSTRING" which could be mapped to something of that form when used on Oracle for a TEXT column, but then again they may just use standard SQL. There is no standard-compliant way to insist on that

Rapacious answered 19/10, 2011 at 13:6 Comment(1)
So I guess I have to resort to a JPA Native Query using string concatenation and forgo type safety. Ouch.Glace
L
10

Criteria supports a function() API that allows a database function to be called by name.

qb.gt(qb.function("CONTAINS", root.get("name"), qb.parameter("name"), qb.literal(1)), 1)

EclipseLink also supports this in JPQL using the FUNC keyword.

Lialiabilities answered 29/11, 2011 at 18:24 Comment(2)
That gives me SQL Error: ORA-29909: label for the ancillary operator is not a literal number. Any ideas?Frustrated
Found it. query.setHint(QueryHints.BIND_PARAMETERS, HintValues.FALSE);Frustrated
R
3

Doubt it. The API is there across all RDBMS, and provides certain constructs like "LIKE"/"SUBSTRING" which could be mapped to something of that form when used on Oracle for a TEXT column, but then again they may just use standard SQL. There is no standard-compliant way to insist on that

Rapacious answered 19/10, 2011 at 13:6 Comment(1)
So I guess I have to resort to a JPA Native Query using string concatenation and forgo type safety. Ouch.Glace
B
2

I just wrote an OracleTextDictionary for openjpa, that converts ordinary 'like' operators into 'contains' operators, when the argument is prefixed with a "magical" marker.

In this way, it's possible to use QueryDSL or Criteria Language (or JPQL) with Oracle text.

The dictionary detects LIKE statements with a magic marker in the argument, and rewrites the SQL to use a CTX CONTAINS call.

One drawback is that the score is not accessible in a simple way, but it would be possible to enhance the driver to order by the score. Feel free to edit the code :-)

I would suppose it's possible to port to hibernate, assuming there is a similar mechanism for tuning database queries to a specific db.

package se.grynna.dict;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.openjpa.jdbc.kernel.JDBCFetchConfiguration;
import org.apache.openjpa.jdbc.sql.OracleDictionary;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.jdbc.sql.Select;

public class OracleTextDictionary extends OracleDictionary {

    public static final String CTX_MAGIC_MARKER = "@CTX@";
    final static Pattern likePattern = Pattern
        .compile("t(\\d+)\\.(\\S+) LIKE (\\?)");


    @Override
    protected SQLBuffer toSelect(SQLBuffer select,
      JDBCFetchConfiguration fetch, SQLBuffer tables, SQLBuffer where,
      SQLBuffer group, SQLBuffer having, SQLBuffer order,
      boolean distinct, boolean forUpdate, long start, long end,Select sel) {

        SQLBuffer sqlBuffer = super.toSelect(select, fetch, tables, where,
          group, having, order, distinct, forUpdate, start, end, sel);

        SQLBuffer tmpBuf = sqlBuffer;

        String sql = tmpBuf.getSQL();

        int label = 1;

        for (Matcher m = likePattern.matcher(sql); m.find(); sql = tmpBuf.getSQL()) {


        int argPos = m.start(3);
        int argIdx = findArgIdx(sql, argPos);
        Object o = tmpBuf.getParameters().get(argIdx);
        if( o == null) break;
        String arg = o.toString();

        if (arg.startsWith(CTX_MAGIC_MARKER)) {

            if (tmpBuf == sqlBuffer) {
                tmpBuf = new SQLBuffer(sqlBuffer);
            }


        arg = arg.substring(CTX_MAGIC_MARKER.length());
        setParameter(tmpBuf, argIdx, arg);

        String aliasNo = m.group(1);
        String colName = m.group(2);

        }

        String replace = String.format("(CONTAINS(t%s.%s,?,%d)>0)",
                    aliasNo, colName, label++);
        tmpBuf.replaceSqlString(m.start(), m.end(), replace);
                m.reset(tmpBuf.getSQL());
        }

      }

    return tmpBuf;
    }

    @SuppressWarnings("unchecked")
    private void setParameter(SQLBuffer tmpBuf, int argIdx, String arg) {
        tmpBuf.getParameters().set(argIdx, arg);

    }

    private int findArgIdx(String sql, int argPos) {
        int count = -1;
        for (int i = 0; i <= argPos; i++) {
            char c = sql.charAt(i);
            if (c == '?') {
                count++;
            }
        }
        return count;
    }



}

Example: The following (obviously contrived) input produces is called with the parameters:

:1 "@CTX@omg near ponies"
:2 "@CTX@rainbow"
:3 "@CTX@rain%"
:4 "abc1%"                     <-- an ordinary like :-)
:5 "@CTX@mushroom%"  

JPQL

select distinct customer
from Customer customer
where customer.custName like :a1 and customer.custName like :a2 and customer.custName like :a1 and customer.custId in (select d.custId
from Customer d
where d.custName like :a3 or d.custName like :a1)

SQL

SELECT t0.custId,
  t0.custName
FROM Customer t0
WHERE ((CONTAINS(t0.custName,?,1)>1)
AND (CONTAINS(t0.custName,?,2)   >1)
AND (CONTAINS(t0.custName,?,3)   >1)
AND t0.custId                   IN
  (SELECT t1.custId
  FROM Customer t1
  WHERE (t1.custName LIKE ?              <---- the like survives....
  OR (CONTAINS(t1.custName,?,1)>1))
  ))
AND ROWNUM <= ?

As a side note: QueryDsl actually does have a ´contains' operator, supposedly for the Lucene backend, for which the jpa and sql backends generates a 'like' statement.

I haven't figured out a way of overloading the contains operator, so that it can be used. (Other than rewriting the code, which I can't do since I am using the version bundled with WebSphere.)

So, I resort to a small static method to make it look good when using QuertyDSL.

// x.where(c.custName.like(CTX.contains("omg near ponies"))));

It would be even nicer if jpql could provide some abstractions (or plugins) for full text search engines...

Binky answered 28/11, 2011 at 0:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.