why does hibernate hql distinct cause an sql distinct on left join? - sql
I've got this test HQL:
select distinct o from Order o left join fetch o.lineItems
and it does generate an SQL distinct without an obvious reason:
select distinct order0_.id as id61_0_, orderline1_.order_id as order1_62_1_...
The SQL resultset is always the same (with and without an SQL distinct):
order id | order name | orderline id | orderline name
---------+------------+--------------+---------------
1 | foo | 1 | foo item
1 | foo | 2 | bar item
1 | foo | 3 | test item
2 | empty | NULL | NULL
3 | bar | 4 | qwerty item
3 | bar | 5 | asdfgh item
Why does hibernate generate the SQL distinct? The SQL distinct doesn't make any sense and makes the query slower than needed.
This is contrary to the FAQ which mentions that hql distinct in this case is just a shortcut for the result transformer:
session.createQuery("select distinct o
from Order o left join fetch
o.lineItems").list();
It looks like you are using the SQL DISTINCT keyword here. Of course, this is not SQL, this is HQL. This distinct is just a shortcut for the result transformer, in this case. Yes, in other cases an HQL distinct will translate straight into a SQL DISTINCT. Not in this case: you can not filter out duplicates at the SQL level, the very nature of a product/join forbids this - you want the duplicates or you don't get all the data you need.
thanks
Have a closer look at the sql statement that hibernate generates - yes it does use the "distinct" keyword but not in the way I think you are expecting it to (or the way that the Hibernate FAQ is implying) i.e. to return a set of "distinct" or "unique" orders.
It doesn't use the distinct keyword to return distinct orders, as that wouldn't make sense in that SQL query, considering the join that you have also specified.
The resulting sql set still needs processing by the ResultTransformer, as clearly the sql set contains duplicate orders. That's why they say that the HQL distinct keyword doesn't directly map to the SQL distinct keyword.
I had the exact same problem and I think this is an Hibernate issue (not a bug because code doesn't fail). However, I have to dig deeper to make sure it's an issue.
Hibernate (at least in version 4 which its the version I'm working on my project, specifically 4.3.11) uses the concept of SPI, long story short: its like an API to extend or modify the framework.
I took advantage of this feature to replace the classes org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory (This class is called by Hibernate and delegates the job of generating the SQL query) and org.hibernate.hql.internal.ast.QueryTranslatorImpl (This is sort of an internal class which is called by org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory and generates the actual SQL query). I did it as follows:
Replacement for org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory:
package org.hibernate.hql.internal.ast;
import java.util.Map;
import org.hibernate.engine.query.spi.EntityGraphQueryHint;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.hql.spi.QueryTranslator;
public class NoDistinctInSQLASTQueryTranslatorFactory extends ASTQueryTranslatorFactory {
#Override
public QueryTranslator createQueryTranslator(String queryIdentifier, String queryString, Map filters, SessionFactoryImplementor factory, EntityGraphQueryHint entityGraphQueryHint) {
return new NoDistinctInSQLQueryTranslatorImpl(queryIdentifier, queryString, filters, factory, entityGraphQueryHint);
}
}
Replacement for org.hibernate.hql.internal.ast.QueryTranslatorImpl:
package org.hibernate.hql.internal.ast;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.QueryException;
import org.hibernate.ScrollableResults;
import org.hibernate.engine.query.spi.EntityGraphQueryHint;
import org.hibernate.engine.spi.QueryParameters;
import org.hibernate.engine.spi.RowSelection;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.event.spi.EventSource;
import org.hibernate.hql.internal.QueryExecutionRequestException;
import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes;
import org.hibernate.hql.internal.antlr.HqlTokenTypes;
import org.hibernate.hql.internal.antlr.SqlTokenTypes;
import org.hibernate.hql.internal.ast.exec.BasicExecutor;
import org.hibernate.hql.internal.ast.exec.DeleteExecutor;
import org.hibernate.hql.internal.ast.exec.MultiTableDeleteExecutor;
import org.hibernate.hql.internal.ast.exec.MultiTableUpdateExecutor;
import org.hibernate.hql.internal.ast.exec.StatementExecutor;
import org.hibernate.hql.internal.ast.tree.AggregatedSelectExpression;
import org.hibernate.hql.internal.ast.tree.FromElement;
import org.hibernate.hql.internal.ast.tree.InsertStatement;
import org.hibernate.hql.internal.ast.tree.QueryNode;
import org.hibernate.hql.internal.ast.tree.Statement;
import org.hibernate.hql.internal.ast.util.ASTPrinter;
import org.hibernate.hql.internal.ast.util.ASTUtil;
import org.hibernate.hql.internal.ast.util.NodeTraverser;
import org.hibernate.hql.spi.FilterTranslator;
import org.hibernate.hql.spi.ParameterTranslations;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.IdentitySet;
import org.hibernate.loader.hql.QueryLoader;
import org.hibernate.param.ParameterSpecification;
import org.hibernate.persister.entity.Queryable;
import org.hibernate.type.Type;
import org.jboss.logging.Logger;
import antlr.ANTLRException;
import antlr.RecognitionException;
import antlr.TokenStreamException;
import antlr.collections.AST;
/**
* A QueryTranslator that uses an Antlr-based parser.
*
* #author Joshua Davis (pgmjsd#sourceforge.net)
*/
public class NoDistinctInSQLQueryTranslatorImpl extends QueryTranslatorImpl implements FilterTranslator {
private static final CoreMessageLogger LOG = Logger.getMessageLogger(
CoreMessageLogger.class,
QueryTranslatorImpl.class.getName()
);
private SessionFactoryImplementor factory;
private final String queryIdentifier;
private String hql;
private boolean shallowQuery;
private Map tokenReplacements;
//TODO:this is only needed during compilation .. can we eliminate the instvar?
private Map enabledFilters;
private boolean compiled;
private QueryLoader queryLoader;
private StatementExecutor statementExecutor;
private Statement sqlAst;
private String sql;
private ParameterTranslations paramTranslations;
private List<ParameterSpecification> collectedParameterSpecifications;
private EntityGraphQueryHint entityGraphQueryHint;
/**
* Creates a new AST-based query translator.
*
* #param queryIdentifier The query-identifier (used in stats collection)
* #param query The hql query to translate
* #param enabledFilters Currently enabled filters
* #param factory The session factory constructing this translator instance.
*/
public NoDistinctInSQLQueryTranslatorImpl(
String queryIdentifier,
String query,
Map enabledFilters,
SessionFactoryImplementor factory) {
super(queryIdentifier, query, enabledFilters, factory);
this.queryIdentifier = queryIdentifier;
this.hql = query;
this.compiled = false;
this.shallowQuery = false;
this.enabledFilters = enabledFilters;
this.factory = factory;
}
public NoDistinctInSQLQueryTranslatorImpl(
String queryIdentifier,
String query,
Map enabledFilters,
SessionFactoryImplementor factory,
EntityGraphQueryHint entityGraphQueryHint) {
this(queryIdentifier, query, enabledFilters, factory);
this.entityGraphQueryHint = entityGraphQueryHint;
}
/**
* Compile a "normal" query. This method may be called multiple times.
* Subsequent invocations are no-ops.
*
* #param replacements Defined query substitutions.
* #param shallow Does this represent a shallow (scalar or entity-id)
* select?
* #throws QueryException There was a problem parsing the query string.
* #throws MappingException There was a problem querying defined mappings.
*/
#Override
public void compile(
Map replacements,
boolean shallow) throws QueryException, MappingException {
doCompile(replacements, shallow, null);
}
/**
* Compile a filter. This method may be called multiple times. Subsequent
* invocations are no-ops.
*
* #param collectionRole the role name of the collection used as the basis
* for the filter.
* #param replacements Defined query substitutions.
* #param shallow Does this represent a shallow (scalar or entity-id)
* select?
* #throws QueryException There was a problem parsing the query string.
* #throws MappingException There was a problem querying defined mappings.
*/
#Override
public void compile(
String collectionRole,
Map replacements,
boolean shallow) throws QueryException, MappingException {
doCompile(replacements, shallow, collectionRole);
}
/**
* Performs both filter and non-filter compiling.
*
* #param replacements Defined query substitutions.
* #param shallow Does this represent a shallow (scalar or entity-id)
* select?
* #param collectionRole the role name of the collection used as the basis
* for the filter, NULL if this is not a filter.
*/
private synchronized void doCompile(Map replacements, boolean shallow, String collectionRole) {
// If the query is already compiled, skip the compilation.
if (compiled) {
LOG.debug("compile() : The query is already compiled, skipping...");
return;
}
// Remember the parameters for the compilation.
this.tokenReplacements = replacements;
if (tokenReplacements == null) {
tokenReplacements = new HashMap();
}
this.shallowQuery = shallow;
try {
// PHASE 1 : Parse the HQL into an AST.
final HqlParser parser = parse(true);
// PHASE 2 : Analyze the HQL AST, and produce an SQL AST.
final HqlSqlWalker w = analyze(parser, collectionRole);
sqlAst = (Statement) w.getAST();
// at some point the generate phase needs to be moved out of here,
// because a single object-level DML might spawn multiple SQL DML
// command executions.
//
// Possible to just move the sql generation for dml stuff, but for
// consistency-sake probably best to just move responsiblity for
// the generation phase completely into the delegates
// (QueryLoader/StatementExecutor) themselves. Also, not sure why
// QueryLoader currently even has a dependency on this at all; does
// it need it? Ideally like to see the walker itself given to the delegates directly...
if (sqlAst.needsExecutor()) {
statementExecutor = buildAppropriateStatementExecutor(w);
} else {
// PHASE 3 : Generate the SQL.
generate((QueryNode) sqlAst);
queryLoader = new QueryLoader(this, factory, w.getSelectClause());
}
compiled = true;
} catch (QueryException qe) {
if (qe.getQueryString() == null) {
throw qe.wrapWithQueryString(hql);
} else {
throw qe;
}
} catch (RecognitionException e) {
// we do not actually propagate ANTLRExceptions as a cause, so
// log it here for diagnostic purposes
LOG.trace("Converted antlr.RecognitionException", e);
throw QuerySyntaxException.convert(e, hql);
} catch (ANTLRException e) {
// we do not actually propagate ANTLRExceptions as a cause, so
// log it here for diagnostic purposes
LOG.trace("Converted antlr.ANTLRException", e);
throw new QueryException(e.getMessage(), hql);
}
//only needed during compilation phase...
this.enabledFilters = null;
}
private void generate(AST sqlAst) throws QueryException, RecognitionException {
if (sql == null) {
final SqlGenerator gen = new SqlGenerator(factory);
gen.statement(sqlAst);
sql = gen.getSQL();
//Hack: The distinct operator is removed from the sql
//string to avoid executing a distinct query in the db server when
//the distinct is used in hql.
sql = sql.replace("distinct", "");
if (LOG.isDebugEnabled()) {
LOG.debugf("HQL: %s", hql);
LOG.debugf("SQL: %s", sql);
}
gen.getParseErrorHandler().throwQueryException();
collectedParameterSpecifications = gen.getCollectedParameters();
}
}
private static final ASTPrinter SQL_TOKEN_PRINTER = new ASTPrinter(SqlTokenTypes.class);
private HqlSqlWalker analyze(HqlParser parser, String collectionRole) throws QueryException, RecognitionException {
final HqlSqlWalker w = new HqlSqlWalker(this, factory, parser, tokenReplacements, collectionRole);
final AST hqlAst = parser.getAST();
// Transform the tree.
w.statement(hqlAst);
if (LOG.isDebugEnabled()) {
LOG.debug(SQL_TOKEN_PRINTER.showAsString(w.getAST(), "--- SQL AST ---"));
}
w.getParseErrorHandler().throwQueryException();
return w;
}
private HqlParser parse(boolean filter) throws TokenStreamException, RecognitionException {
// Parse the query string into an HQL AST.
final HqlParser parser = HqlParser.getInstance(hql);
parser.setFilter(filter);
LOG.debugf("parse() - HQL: %s", hql);
parser.statement();
final AST hqlAst = parser.getAST();
final NodeTraverser walker = new NodeTraverser(new JavaConstantConverter());
walker.traverseDepthFirst(hqlAst);
showHqlAst(hqlAst);
parser.getParseErrorHandler().throwQueryException();
return parser;
}
private static final ASTPrinter HQL_TOKEN_PRINTER = new ASTPrinter(HqlTokenTypes.class);
#Override
void showHqlAst(AST hqlAst) {
if (LOG.isDebugEnabled()) {
LOG.debug(HQL_TOKEN_PRINTER.showAsString(hqlAst, "--- HQL AST ---"));
}
}
private void errorIfDML() throws HibernateException {
if (sqlAst.needsExecutor()) {
throw new QueryExecutionRequestException("Not supported for DML operations", hql);
}
}
private void errorIfSelect() throws HibernateException {
if (!sqlAst.needsExecutor()) {
throw new QueryExecutionRequestException("Not supported for select queries", hql);
}
}
#Override
public String getQueryIdentifier() {
return queryIdentifier;
}
#Override
public Statement getSqlAST() {
return sqlAst;
}
private HqlSqlWalker getWalker() {
return sqlAst.getWalker();
}
/**
* Types of the return values of an <tt>iterate()</tt> style query.
*
* #return an array of <tt>Type</tt>s.
*/
#Override
public Type[] getReturnTypes() {
errorIfDML();
return getWalker().getReturnTypes();
}
#Override
public String[] getReturnAliases() {
errorIfDML();
return getWalker().getReturnAliases();
}
#Override
public String[][] getColumnNames() {
errorIfDML();
return getWalker().getSelectClause().getColumnNames();
}
#Override
public Set<Serializable> getQuerySpaces() {
return getWalker().getQuerySpaces();
}
#Override
public List list(SessionImplementor session, QueryParameters queryParameters)
throws HibernateException {
// Delegate to the QueryLoader...
errorIfDML();
final QueryNode query = (QueryNode) sqlAst;
final boolean hasLimit = queryParameters.getRowSelection() != null && queryParameters.getRowSelection().definesLimits();
final boolean needsDistincting = (query.getSelectClause().isDistinct() || hasLimit) && containsCollectionFetches();
QueryParameters queryParametersToUse;
if (hasLimit && containsCollectionFetches()) {
LOG.firstOrMaxResultsSpecifiedWithCollectionFetch();
RowSelection selection = new RowSelection();
selection.setFetchSize(queryParameters.getRowSelection().getFetchSize());
selection.setTimeout(queryParameters.getRowSelection().getTimeout());
queryParametersToUse = queryParameters.createCopyUsing(selection);
} else {
queryParametersToUse = queryParameters;
}
List results = queryLoader.list(session, queryParametersToUse);
if (needsDistincting) {
int includedCount = -1;
// NOTE : firstRow is zero-based
int first = !hasLimit || queryParameters.getRowSelection().getFirstRow() == null
? 0
: queryParameters.getRowSelection().getFirstRow();
int max = !hasLimit || queryParameters.getRowSelection().getMaxRows() == null
? -1
: queryParameters.getRowSelection().getMaxRows();
List tmp = new ArrayList();
IdentitySet distinction = new IdentitySet();
for (final Object result : results) {
if (!distinction.add(result)) {
continue;
}
includedCount++;
if (includedCount < first) {
continue;
}
tmp.add(result);
// NOTE : ( max - 1 ) because first is zero-based while max is not...
if (max >= 0 && (includedCount - first) >= (max - 1)) {
break;
}
}
results = tmp;
}
return results;
}
/**
* Return the query results as an iterator
*/
#Override
public Iterator iterate(QueryParameters queryParameters, EventSource session)
throws HibernateException {
// Delegate to the QueryLoader...
errorIfDML();
return queryLoader.iterate(queryParameters, session);
}
/**
* Return the query results, as an instance of <tt>ScrollableResults</tt>
*/
#Override
public ScrollableResults scroll(QueryParameters queryParameters, SessionImplementor session)
throws HibernateException {
// Delegate to the QueryLoader...
errorIfDML();
return queryLoader.scroll(queryParameters, session);
}
#Override
public int executeUpdate(QueryParameters queryParameters, SessionImplementor session)
throws HibernateException {
errorIfSelect();
return statementExecutor.execute(queryParameters, session);
}
/**
* The SQL query string to be called; implemented by all subclasses
*/
#Override
public String getSQLString() {
return sql;
}
#Override
public List<String> collectSqlStrings() {
ArrayList<String> list = new ArrayList<>();
if (isManipulationStatement()) {
String[] sqlStatements = statementExecutor.getSqlStatements();
Collections.addAll(list, sqlStatements);
} else {
list.add(sql);
}
return list;
}
// -- Package local methods for the QueryLoader delegate --
#Override
public boolean isShallowQuery() {
return shallowQuery;
}
#Override
public String getQueryString() {
return hql;
}
#Override
public Map getEnabledFilters() {
return enabledFilters;
}
#Override
public int[] getNamedParameterLocs(String name) {
return getWalker().getNamedParameterLocations(name);
}
#Override
public boolean containsCollectionFetches() {
errorIfDML();
List collectionFetches = ((QueryNode) sqlAst).getFromClause().getCollectionFetches();
return collectionFetches != null && collectionFetches.size() > 0;
}
#Override
public boolean isManipulationStatement() {
return sqlAst.needsExecutor();
}
#Override
public void validateScrollability() throws HibernateException {
// Impl Note: allows multiple collection fetches as long as the
// entire fecthed graph still "points back" to a single
// root entity for return
errorIfDML();
final QueryNode query = (QueryNode) sqlAst;
// If there are no collection fetches, then no further checks are needed
List collectionFetches = query.getFromClause().getCollectionFetches();
if (collectionFetches.isEmpty()) {
return;
}
// A shallow query is ok (although technically there should be no fetching here...)
if (isShallowQuery()) {
return;
}
// Otherwise, we have a non-scalar select with defined collection fetch(es).
// Make sure that there is only a single root entity in the return (no tuples)
if (getReturnTypes().length > 1) {
throw new HibernateException("cannot scroll with collection fetches and returned tuples");
}
FromElement owner = null;
for (Object o : query.getSelectClause().getFromElementsForLoad()) {
// should be the first, but just to be safe...
final FromElement fromElement = (FromElement) o;
if (fromElement.getOrigin() == null) {
owner = fromElement;
break;
}
}
if (owner == null) {
throw new HibernateException("unable to locate collection fetch(es) owner for scrollability checks");
}
// This is not strictly true. We actually just need to make sure that
// it is ordered by root-entity PK and that that order-by comes before
// any non-root-entity ordering...
AST primaryOrdering = query.getOrderByClause().getFirstChild();
if (primaryOrdering != null) {
// TODO : this is a bit dodgy, come up with a better way to check this (plus see above comment)
String[] idColNames = owner.getQueryable().getIdentifierColumnNames();
String expectedPrimaryOrderSeq = StringHelper.join(
", ",
StringHelper.qualify(owner.getTableAlias(), idColNames)
);
if (!primaryOrdering.getText().startsWith(expectedPrimaryOrderSeq)) {
throw new HibernateException("cannot scroll results with collection fetches which are not ordered primarily by the root entity's PK");
}
}
}
private StatementExecutor buildAppropriateStatementExecutor(HqlSqlWalker walker) {
final Statement statement = (Statement) walker.getAST();
switch (walker.getStatementType()) {
case HqlSqlTokenTypes.DELETE: {
final FromElement fromElement = walker.getFinalFromClause().getFromElement();
final Queryable persister = fromElement.getQueryable();
if (persister.isMultiTable()) {
return new MultiTableDeleteExecutor(walker);
} else {
return new DeleteExecutor(walker, persister);
}
}
case HqlSqlTokenTypes.UPDATE: {
final FromElement fromElement = walker.getFinalFromClause().getFromElement();
final Queryable persister = fromElement.getQueryable();
if (persister.isMultiTable()) {
// even here, if only properties mapped to the "base table" are referenced
// in the set and where clauses, this could be handled by the BasicDelegate.
// TODO : decide if it is better performance-wise to doAfterTransactionCompletion that check, or to simply use the MultiTableUpdateDelegate
return new MultiTableUpdateExecutor(walker);
} else {
return new BasicExecutor(walker, persister);
}
}
case HqlSqlTokenTypes.INSERT:
return new BasicExecutor(walker, ((InsertStatement) statement).getIntoClause().getQueryable());
default:
throw new QueryException("Unexpected statement type");
}
}
#Override
public ParameterTranslations getParameterTranslations() {
if (paramTranslations == null) {
paramTranslations = new ParameterTranslationsImpl(getWalker().getParameters());
}
return paramTranslations;
}
#Override
public List<ParameterSpecification> getCollectedParameterSpecifications() {
return collectedParameterSpecifications;
}
#Override
public Class getDynamicInstantiationResultType() {
AggregatedSelectExpression aggregation = queryLoader.getAggregatedSelectExpression();
return aggregation == null ? null : aggregation.getAggregationResultType();
}
public static class JavaConstantConverter implements NodeTraverser.VisitationStrategy {
private AST dotRoot;
#Override
public void visit(AST node) {
if (dotRoot != null) {
// we are already processing a dot-structure
if (ASTUtil.isSubtreeChild(dotRoot, node)) {
return;
}
// we are now at a new tree level
dotRoot = null;
}
if (node.getType() == HqlTokenTypes.DOT) {
dotRoot = node;
handleDotStructure(dotRoot);
}
}
private void handleDotStructure(AST dotStructureRoot) {
final String expression = ASTUtil.getPathText(dotStructureRoot);
final Object constant = ReflectHelper.getConstantValue(expression);
if (constant != null) {
dotStructureRoot.setFirstChild(null);
dotStructureRoot.setType(HqlTokenTypes.JAVA_CONSTANT);
dotStructureRoot.setText(expression);
}
}
}
#Override
public EntityGraphQueryHint getEntityGraphQueryHint() {
return entityGraphQueryHint;
}
#Override
public void setEntityGraphQueryHint(EntityGraphQueryHint entityGraphQueryHint) {
this.entityGraphQueryHint = entityGraphQueryHint;
}
}
If you follow the code flow you will notice that I just modified the method private void generate(AST sqlAst) throws QueryException, RecognitionException and added the a following lines:
//Hack: The distinct keywordis removed from the sql string to
//avoid executing a distinct query in the DBMS when the distinct
//is used in hql.
sql = sql.replace("distinct", "");
What I do with this code is to remove the distinct keyword from the generated SQL query.
After creating the classes above, I added the following line in the hibernate configuration file:
<property name="hibernate.query.factory_class">org.hibernate.hql.internal.ast.NoDistinctInSQLASTQueryTranslatorFactory</property>
This line tells hibernate to use my custom class to parse HQL queries and generate SQL queries without the distinct keyword. Notice I created my custom classes in the same package where the original HQL parser resides.
Related
What happens if no records exists when I use Room to query?
I use Room in my Android Studio App, the Code A will crash when no record exists to query What happens if no records exists when I use Room to query with Code B ? #Dao interface RecordDao { // Code A #Query("SELECT * FROM record_table where id=:id") fun getByID(id:Int): Flow<RecordEntity> // Code B #Query("SELECT * FROM record_table") fun getAll(): Flow<List<RecordEntity>> }
If you look at the code generated by room for the RecordDao_Impl class implementation of RecordDao, you'll notice multiple things: The getById function code returns null when no matching records exist in the table, and since your 'Code A' function return type is not null, kotlin throws a NullPointerException for the first function. The getAll function code returns a Flow object with an ArrayList, in case it found any records then it adds them to the list, otherwise it just emits the empty ArrayList to the Flow object, therefore, you'll always get a flow object with a list inside it regardless if room found matching records or not, so no exception is thrown. You can understand this a bit more if you look at the code generated for the two functions here: #Override public Flow<RecordEntity> getByID(final int id) { final String _sql = "SELECT * FROM record_table where id=?"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1); int _argIndex = 1; _statement.bindLong(_argIndex, id); return CoroutinesRoom.createFlow(__db, false, new String[]{"record_table"}, new Callable<RecordEntity>() { #Override public RecordEntity call() throws Exception { final Cursor _cursor = DBUtil.query(__db, _statement, false, null); try { final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id"); final RecordEntity _result; if (_cursor.moveToFirst()) { final int _tmpId; _tmpId = _cursor.getInt(_cursorIndexOfId); _result = new RecordEntity(_tmpId); } else { _result = null; } return _result; } finally { _cursor.close(); } } #Override protected void finalize() { _statement.release(); } }); } #Override public Flow<List<RecordEntity>> getAll() { final String _sql = "SELECT * FROM record_table"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0); return CoroutinesRoom.createFlow(__db, false, new String[]{"record_table"}, new Callable<List<RecordEntity>>() { #Override public List<RecordEntity> call() throws Exception { final Cursor _cursor = DBUtil.query(__db, _statement, false, null); try { final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id"); final List<RecordEntity> _result = new ArrayList<RecordEntity>(_cursor.getCount()); while (_cursor.moveToNext()) { final RecordEntity _item; final int _tmpId; _tmpId = _cursor.getInt(_cursorIndexOfId); _item = new RecordEntity(_tmpId); _result.add(_item); } return _result; } finally { _cursor.close(); } } #Override protected void finalize() { _statement.release(); } }); }
Spring AOP - passing arguments between annotated methods
i've written a utility to monitor individual business transactions. For example, Alice calls a method which calls more methods and i want info on just Alice's call, separate from Bob's call to the same method. Right now the entry point creates a Transaction object and it's passed as an argument to each method: class Example { public Item getOrderEntryPoint(int orderId) { Transaction transaction = transactionManager.create(); transaction.trace("getOrderEntryPoint"); Order order = getOrder(orderId, transaction); transaction.stop(); logger.info(transaction); return item; } private Order getOrder(int orderId, Transaction t) { t.trace("getOrder"); Order order = getItems(itemId, t); t.addStat("number of items", order.getItems().size()); for (Item item : order.getItems()) { SpecialOffer offer = getSpecialOffer(item, t); if (null != offer) { t.incrementStat("offers", 1); } } t.stop(); return order; } private SpecialOffer getSpecialOffer(Item item, Transaction t) { t.trace("getSpecialOffer(" + item.id + ")", TraceCategory.Database); return offerRepository.getByItem(item); t.stop(); } } This will print to the log something like: Transaction started by Alice at 10:42 Statistics: number of items : 3 offers : 1 Category Timings (longest first): DB : 2s 903ms code : 187ms Timings (longest first): getSpecialOffer(1013) : 626ms getItems : 594ms Trace: getOrderEntryPoint (7ms) getOrder (594ms) getSpecialOffer(911) (90ms) getSpecialOffer(1013) (626ms) getSpecialOffer(2942) (113ms) It works great but passing the transaction object around is ugly. Someone suggested AOP but i don't see how to pass the transaction created in the first method to all the other methods. The Transaction object is pretty simple: public class Transaction { private String uuid = UUID.createRandom(); private List<TraceEvent> events = new ArrayList<>(); private Map<String,Int> stats = new HashMap<>(); } class TraceEvent { private String name; private long durationInMs; } The app that uses it is a Web app, and this multi-threaded, but the individual transactions are on a single thread - no multi-threading, async code, competition for resources, etc. My attempt at an annotation: #Around("execution(* *(..)) && #annotation(Trace)") public Object around(ProceedingJoinPoint point) { String methodName = MethodSignature.class.cast(point.getSignature()).getMethod().getName(); //--- Where do i get this call's instance of TRANSACTION from? if (null == transaction) { transaction = TransactionManager.createTransaction(); } transaction.trace(methodName); Object result = point.proceed(); transaction.stop(); return result;
Introduction Unfortunately, your pseudo code does not compile. It contains several syntactical and logical errors. Furthermore, some helper classes are missing. If I did not have spare time today and was looking for a puzzle to solve, I would not have bothered making my own MCVE out of it, because that would actually have been your job. Please do read the MCVE article and learn to create one next time, otherwise you will not get a lot of qualified help here. This was your free shot because you are new on SO. Original situation: passing through transaction objects in method calls Application helper classes: package de.scrum_master.app; public class Item { private int id; public Item(int id) { this.id = id; } public int getId() { return id; } #Override public String toString() { return "Item[id=" + id + "]"; } } package de.scrum_master.app; public class SpecialOffer {} package de.scrum_master.app; public class OfferRepository { public SpecialOffer getByItem(Item item) { if (item.getId() < 30) return new SpecialOffer(); return null; } } package de.scrum_master.app; import java.util.ArrayList; import java.util.List; public class Order { private int id; public Order(int id) { this.id = id; } public List<Item> getItems() { List<Item> items = new ArrayList<>(); int offset = id == 12345 ? 0 : 1; items.add(new Item(11 + offset, this)); items.add(new Item(22 + offset, this)); items.add(new Item(33 + offset, this)); return items; } } Trace classes: package de.scrum_master.trace; public enum TraceCategory { Code, Database } package de.scrum_master.trace; class TraceEvent { private String name; private TraceCategory category; private long durationInMs; private boolean finished = false; public TraceEvent(String name, TraceCategory category, long startTime) { this.name = name; this.category = category; this.durationInMs = startTime; } public long getDurationInMs() { return durationInMs; } public void setDurationInMs(long durationInMs) { this.durationInMs = durationInMs; } public boolean isFinished() { return finished; } public void setFinished(boolean finished) { this.finished = finished; } #Override public String toString() { return "TraceEvent[name=" + name + ", category=" + category + ", durationInMs=" + durationInMs + ", finished=" + finished + "]"; } } Transaction classes: Here I tried to mimic your own Transaction class with as few as possible changes, but there was a lot I had to add and modify in order to emulate a simplified version of your trace output. This is not thread-safe and the way I am locating the last unfinished TraceEvent is not nice and only works cleanly if there are not exceptions. But you get the idea, I hope. The point is to just make it basically work and subsequently get log output similar to your example. If this was originally my code, I would have solved it differently. package de.scrum_master.trace; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; public class Transaction { private String uuid = UUID.randomUUID().toString(); private List<TraceEvent> events = new ArrayList<>(); private Map<String, Integer> stats = new HashMap<>(); public void trace(String message) { trace(message, TraceCategory.Code); } public void trace(String message, TraceCategory category) { events.add(new TraceEvent(message, category, System.currentTimeMillis())); } public void stop() { TraceEvent event = getLastUnfinishedEvent(); event.setDurationInMs(System.currentTimeMillis() - event.getDurationInMs()); event.setFinished(true); } private TraceEvent getLastUnfinishedEvent() { return events .stream() .filter(event -> !event.isFinished()) .reduce((first, second) -> second) .orElse(null); } public void addStat(String text, int size) { stats.put(text, size); } public void incrementStat(String text, int increment) { Integer currentCount = stats.get(text); if (currentCount == null) currentCount = 0; stats.put(text, currentCount + increment); } #Override public String toString() { return "Transaction {" + toStringUUID() + toStringStats() + toStringEvents() + "\n}\n"; } private String toStringUUID() { return "\n uuid = " + uuid; } private String toStringStats() { String result = "\n stats = {"; for (Entry<String, Integer> statEntry : stats.entrySet()) result += "\n " + statEntry; return result + "\n }"; } private String toStringEvents() { String result = "\n events = {"; for (TraceEvent event : events) result += "\n " + event; return result + "\n }"; } } package de.scrum_master.trace; public class TransactionManager { public Transaction create() { return new Transaction(); } } Example driver application: package de.scrum_master.app; import de.scrum_master.trace.TraceCategory; import de.scrum_master.trace.Transaction; import de.scrum_master.trace.TransactionManager; public class Example { private TransactionManager transactionManager = new TransactionManager(); private OfferRepository offerRepository = new OfferRepository(); public Order getOrderEntryPoint(int orderId) { Transaction transaction = transactionManager.create(); transaction.trace("getOrderEntryPoint"); sleep(100); Order order = getOrder(orderId, transaction); transaction.stop(); System.out.println(transaction); return order; } private Order getOrder(int orderId, Transaction t) { t.trace("getOrder"); sleep(200); Order order = new Order(orderId); t.addStat("number of items", order.getItems().size()); for (Item item : order.getItems()) { SpecialOffer offer = getSpecialOffer(item, t); if (null != offer) t.incrementStat("special offers", 1); } t.stop(); return order; } private SpecialOffer getSpecialOffer(Item item, Transaction t) { t.trace("getSpecialOffer(" + item.getId() + ")", TraceCategory.Database); sleep(50); SpecialOffer specialOffer = offerRepository.getByItem(item); t.stop(); return specialOffer; } private void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { new Example().getOrderEntryPoint(12345); new Example().getOrderEntryPoint(23456); } } If you run this code, the output is as follows: Transaction { uuid = 62ec9739-bd32-4a56-b6b3-a8a13624961a stats = { special offers=2 number of items=3 } events = { TraceEvent[name=getOrderEntryPoint, category=Code, durationInMs=561, finished=true] TraceEvent[name=getOrder, category=Code, durationInMs=451, finished=true] TraceEvent[name=getSpecialOffer(11), category=Database, durationInMs=117, finished=true] TraceEvent[name=getSpecialOffer(22), category=Database, durationInMs=69, finished=true] TraceEvent[name=getSpecialOffer(33), category=Database, durationInMs=63, finished=true] } } Transaction { uuid = a420cd70-96e5-44c4-a0a4-87e421d05e87 stats = { special offers=2 number of items=3 } events = { TraceEvent[name=getOrderEntryPoint, category=Code, durationInMs=469, finished=true] TraceEvent[name=getOrder, category=Code, durationInMs=369, finished=true] TraceEvent[name=getSpecialOffer(12), category=Database, durationInMs=53, finished=true] TraceEvent[name=getSpecialOffer(23), category=Database, durationInMs=63, finished=true] TraceEvent[name=getSpecialOffer(34), category=Database, durationInMs=53, finished=true] } } AOP refactoring Preface Please note that I am using AspectJ here because two things about your code would never work with Spring AOP because it works with a delegation pattern based on dynamic proxies: self-invocation (internally calling a method of the same class or super-class) intercepting private methods Because of these Spring AOP limitations I advise you to either refactor your code so as to avoid the two issues above or to configure your Spring applications to use full AspectJ via LTW (load-time weaving) instead. As you noticed, my sample code does not use Spring at all because AspectJ is completely independent of Spring and works with any Java application (or other JVM languages, too). Refactoring idea Now what should you do in order to get rid of passing around tracing information (Transaction objects), polluting your core application code and tangling it with trace calls? You extract transaction tracing into an aspect taking care of all trace(..) and stop() calls. Unfortunately your Transaction class contains different types of information and does different things, so you cannot completely get rid of context information about how to trace for each affected method. But at least you can extract that context information from the method bodies and transform it into a declarative form using annotations with parameters. These annotations can be targeted by an aspect taking care of handling transaction tracing. Added and updated code, iteration 1 Annotations related to transaction tracing: package de.scrum_master.trace; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; #Retention(RUNTIME) #Target(METHOD) public #interface TransactionEntryPoint {} package de.scrum_master.trace; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; #Retention(RUNTIME) #Target(METHOD) public #interface TransactionTrace { String message() default "__METHOD_NAME__"; TraceCategory category() default TraceCategory.Code; String addStat() default ""; String incrementStat() default ""; } Refactored application classes with annotations: package de.scrum_master.app; import java.util.ArrayList; import java.util.List; import de.scrum_master.trace.TransactionTrace; public class Order { private int id; public Order(int id) { this.id = id; } #TransactionTrace(message = "", addStat = "number of items") public List<Item> getItems() { List<Item> items = new ArrayList<>(); int offset = id == 12345 ? 0 : 1; items.add(new Item(11 + offset)); items.add(new Item(22 + offset)); items.add(new Item(33 + offset)); return items; } } Nothing much here, only added an annotation to getItems(). But the sample application class changes massively, getting much cleaner and simpler: package de.scrum_master.app; import de.scrum_master.trace.TraceCategory; import de.scrum_master.trace.TransactionEntryPoint; import de.scrum_master.trace.TransactionTrace; public class Example { private OfferRepository offerRepository = new OfferRepository(); #TransactionEntryPoint #TransactionTrace public Order getOrderEntryPoint(int orderId) { sleep(100); Order order = getOrder(orderId); return order; } #TransactionTrace private Order getOrder(int orderId) { sleep(200); Order order = new Order(orderId); for (Item item : order.getItems()) { SpecialOffer offer = getSpecialOffer(item); // Do something with special offers } return order; } #TransactionTrace(category = TraceCategory.Database, incrementStat = "specialOffers") private SpecialOffer getSpecialOffer(Item item) { sleep(50); SpecialOffer specialOffer = offerRepository.getByItem(item); return specialOffer; } private void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { new Example().getOrderEntryPoint(12345); new Example().getOrderEntryPoint(23456); } } See? Except for a few annotations there is nothing left of the transaction tracing logic, the application code only takes care of its core concern. If you also remove the sleep() method which only makes the application slower for demonstration purposes (because we want some nice statistics with measured times >0 ms), the class gets even more compact. But of course we need to put the transaction tracing logic somewhere, more precisely modularise it into an AspectJ aspect: Transaction tracing aspect: package de.scrum_master.trace; import java.lang.reflect.Array; import java.util.Arrays; import java.util.Collection; import java.util.stream.Collectors; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; #Aspect("percflow(entryPoint())") public class TransactionTraceAspect { private static TransactionManager transactionManager = new TransactionManager(); private Transaction transaction = transactionManager.create(); #Pointcut("execution(* *(..)) && #annotation(de.scrum_master.trace.TransactionEntryPoint)") private static void entryPoint() {} #Around("execution(* *(..)) && #annotation(transactionTrace)") public Object doTrace(ProceedingJoinPoint joinPoint, TransactionTrace transactionTrace) throws Throwable { preTrace(transactionTrace, joinPoint); Object result = joinPoint.proceed(); postTrace(transactionTrace); addStat(transactionTrace, result); incrementStat(transactionTrace, result); return result; } private void preTrace(TransactionTrace transactionTrace, ProceedingJoinPoint joinPoint) { String traceMessage = transactionTrace.message(); if ("".equals(traceMessage)) return; MethodSignature signature = (MethodSignature) joinPoint.getSignature(); if ("__METHOD_NAME__".equals(traceMessage)) { traceMessage = signature.getName() + "("; traceMessage += Arrays.stream(joinPoint.getArgs()).map(arg -> arg.toString()).collect(Collectors.joining(", ")); traceMessage += ")"; } transaction.trace(traceMessage, transactionTrace.category()); } private void postTrace(TransactionTrace transactionTrace) { if ("".equals(transactionTrace.message())) return; transaction.stop(); } private void addStat(TransactionTrace transactionTrace, Object result) { if ("".equals(transactionTrace.addStat()) || result == null) return; if (result instanceof Collection) transaction.addStat(transactionTrace.addStat(), ((Collection<?>) result).size()); else if (result.getClass().isArray()) transaction.addStat(transactionTrace.addStat(), Array.getLength(result)); } private void incrementStat(TransactionTrace transactionTrace, Object result) { if ("".equals(transactionTrace.incrementStat()) || result == null) return; transaction.incrementStat(transactionTrace.incrementStat(), 1); } #After("entryPoint()") public void logFinishedTransaction(JoinPoint joinPoint) { System.out.println(transaction); } } Let me explain what this aspect does: #Pointcut(..) entryPoint() says: Find me all methods in the code annotated by #TransactionEntryPoint. This pointcut is used in two places: #Aspect("percflow(entryPoint())") says: Create one aspect instance for each control flow beginning at a transaction entry point. #After("entryPoint()") logFinishedTransaction(..) says: Execute this advice (AOP terminology for a method linked to a pointcut) after an entry point methods is finished. The corresponding method just prints the transaction statistics just like in the original code at the end of Example.getOrderEntryPoint(..). #Around("execution(* *(..)) && #annotation(transactionTrace)") doTrace(..)says: Wrap methods annotated by TransactionTrace and do the following (method body): add new trace element and start measuring time execute original (wrapped) method and store result update trace element with measured time add one type of statistics (optional) increment another type of statistics (optional) return wrapped method's result to its caller The private methods are just helpers for the #Around advice. The console log when running the updated Example class and active AspectJ is: Transaction { uuid = 4529d325-c604-441d-8997-45ca659abb14 stats = { specialOffers=2 number of items=3 } events = { TraceEvent[name=getOrderEntryPoint(12345), category=Code, durationInMs=468, finished=true] TraceEvent[name=getOrder(12345), category=Code, durationInMs=366, finished=true] TraceEvent[name=getSpecialOffer(Item[id=11]), category=Database, durationInMs=59, finished=true] TraceEvent[name=getSpecialOffer(Item[id=22]), category=Database, durationInMs=50, finished=true] TraceEvent[name=getSpecialOffer(Item[id=33]), category=Database, durationInMs=51, finished=true] } } Transaction { uuid = ef76a996-8621-478b-a376-e9f7a729a501 stats = { specialOffers=2 number of items=3 } events = { TraceEvent[name=getOrderEntryPoint(23456), category=Code, durationInMs=452, finished=true] TraceEvent[name=getOrder(23456), category=Code, durationInMs=351, finished=true] TraceEvent[name=getSpecialOffer(Item[id=12]), category=Database, durationInMs=50, finished=true] TraceEvent[name=getSpecialOffer(Item[id=23]), category=Database, durationInMs=50, finished=true] TraceEvent[name=getSpecialOffer(Item[id=34]), category=Database, durationInMs=50, finished=true] } } You see, it looks almost identical to the original application. Idea for further simplification, iteration 2 When reading method Example.getOrder(int orderId) I was wondering why you are calling order.getItems(), looping over it and calling getSpecialOffer(item) inside the loop. In your sample code you do not use the results for anything other than updating the transaction trace object. I am assuming that in your real code you do something with the order and with the special offers in that method. But just in case you really do not need those calls inside that method, I suggest you factor the calls out right into the aspect, getting rid of the TransactionTrace annotation parameters String addStat() and String incrementStat(). The Example code would get even simpler and the annotation #TransactionTrace(message = "", addStat = "number of items") in class would go away, too. I am leaving this refactoring to you if you think it makes sense.
Search where A or B with querydsl and spring data rest
http://localhost:8080/users?firstName=a&lastName=b ---> where firstName=a and lastName=b How to make it to or ---> where firstName=a or lastName=b But when I set QuerydslBinderCustomizer customize #Override default public void customize(QuerydslBindings bindings, QUser user) { bindings.bind(String.class).all((StringPath path, Collection<? extends String> values) -> { BooleanBuilder predicate = new BooleanBuilder(); values.forEach( value -> predicate.or(path.containsIgnoreCase(value) ); }); } http://localhost:8080/users?firstName=a&firstName=b&lastName=b ---> where (firstName=a or firstName = b) and lastName=b It seem different parameters with AND. Same parameters with what I set(predicate.or/predicate.and) How to make it different parameters with AND like this ---> where firstName=a or firstName=b or lastName=b ?? thx.
Your current request param are grouped as List firstName and String lastName. I see that you want to keep your request parameters without a binding, but in this case it would make your life easier. My suggestion is to make a new class with request param: public class UserRequest { private String lastName; private List<String> firstName; // getters and setters } For QueryDSL, you can create a builder object: public class UserPredicateBuilder{ private List<BooleanExpression> expressions = new ArrayList<>(); public UserPredicateBuilder withFirstName(List<String> firstNameList){ QUser user = QUser.user; expressions.add(user.firstName.in(firstNameList)); return this; } //.. same for other fields public BooleanExpression build(){ if(expressions.isEmpty()){ return Expressions.asBoolean(true).isTrue(); } BooleanExpression result = expressions.get(0); for (int i = 1; i < expressions.size(); i++) { result = result.and(expressions.get(i)); } return result; } } And after you can just use the builder as : public List<User> getUsers(UserRequest userRequest){ BooleanExpression expression = new UserPredicateBuilder() .withFirstName(userRequest.getFirstName()) // other fields .build(); return userRepository.findAll(expression).getContent(); } This is the recommended solution. If you really want to keep the current params without a binding (they still need some kind of validation, otherwise it can throw an Exception in query dsl binding) you can group them by path : Map<StringPath,List<String>> values // example firstName => a,b and after that to create your boolean expression based on the map: //initial value BooleanExpression result = Expressions.asBoolean(true).isTrue(); for (Map.Entry entry: values.entrySet()) { result = result.and(entry.getKey().in(entry.getValues()); } return userRepository.findAll(result);
Oracle Coherence index not working with ContainsFilter query
I've added an index to a cache. The index uses a custom extractor that extends AbstractExtractor and overrides only the extract method to return a List of Strings. Then I have a ContainsFilter which uses the same custom extractor that looks for the occurence of a single String in the List of Strings. It does not look like my index is being used based on the time it takes to execute my test. What am I doing wrong? Also, is there some debugging I can switch on to see which indices are used? public class DependencyIdExtractor extends AbstractExtractor { /** * */ private static final long serialVersionUID = 1L; #Override public Object extract(Object oTarget) { if (oTarget == null) { return null; } if (oTarget instanceof CacheValue) { CacheValue cacheValue = (CacheValue)oTarget; // returns a List of String objects return cacheValue.getDependencyIds(); } throw new UnsupportedOperationException(); } } Adding the index: mCache = CacheFactory.getCache(pCacheName); mCache.addIndex(new DependencyIdExtractor(), false, null); Performing the ContainsFilter query: public void invalidateByDependencyId(String pDependencyId) { ContainsFilter vContainsFilter = new ContainsFilter(new DependencyIdExtractor(), pDependencyId); #SuppressWarnings("rawtypes") Set setKeys = mCache.keySet(vContainsFilter); mCache.keySet().removeAll(setKeys); }
I solved this by adding a hashCode and equals method implementation to the DependencyIdExtractor class. It is important that you use exactly the same value extractor when adding an index and creating your filter. public class DependencyIdExtractor extends AbstractExtractor { /** * */ private static final long serialVersionUID = 1L; #Override public Object extract(Object oTarget) { if (oTarget == null) { return null; } if (oTarget instanceof CacheValue) { CacheValue cacheValue = (CacheValue)oTarget; return cacheValue.getDependencyIds(); } throw new UnsupportedOperationException(); } #Override public int hashCode() { return 1; } #Override public boolean equals(Object obj) { if (obj == null) { return false; } if (obj instanceof DependencyIdExtractor) { return true; } return false; } } To debug Coherence indices/queries, you can generate an explain plan similar to database query explain plans. http://www.oracle.com/technetwork/tutorials/tutorial-1841899.html #SuppressWarnings("unchecked") public void invalidateByDependencyId(String pDependencyId) { ContainsFilter vContainsFilter = new ContainsFilter(new DependencyIdExtractor(), pDependencyId); if (mLog.isTraceEnabled()) { QueryRecorder agent = new QueryRecorder(RecordType.EXPLAIN); Object resultsExplain = mCache.aggregate(vContainsFilter, agent); mLog.trace("resultsExplain = \n" + resultsExplain + "\n"); } #SuppressWarnings("rawtypes") Set setKeys = mCache.keySet(vContainsFilter); mCache.keySet().removeAll(setKeys); }
Hibernate Search with AnalyzerDiscriminator - Analyzer called only when creating Entity?
can you help me? I am implementing Hibernate Search, to retrieve results for a global search on a localized website (portuguese and english content) To do this, I have followed the steps indicated on the Hibernate Search docs: http://docs.jboss.org/hibernate/search/4.5/reference/en-US/html_single/#d0e4141 Along with the specific configuration in the entity itself, I have implemented a "LanguageDiscriminator" class, following the instructions in this doc. Because I am not getting exactly the results I was expecting (e.g. my entity has the text "Capuchinho" stored, but when I search for "capucho" I get no hits), I have decided to try and debug the execution, and try to understand if the Analyzers which I have configured are being used at all. When creating a new record for the entity in the database, I can see that the "getAnalyzerDefinitionName()" method from the "LanguageDiscriminator" gets called. Great. But the same does not happen when I execute a search. Can anyone explain me why? I am posting the key parts of my code below. Thanks a lot for any feedback! This is one entity I want to index #Entity #Table(name="NEWS_HEADER") #Indexed #AnalyzerDefs({ #AnalyzerDef(name = "en", tokenizer = #TokenizerDef(factory = StandardTokenizerFactory.class), filters = { #TokenFilterDef(factory = LowerCaseFilterFactory.class), #TokenFilterDef(factory = SnowballPorterFilterFactory.class, params = {#Parameter(name="language", value="English")} ) } ), #AnalyzerDef(name = "pt", tokenizer = #TokenizerDef(factory = StandardTokenizerFactory.class), filters = { #TokenFilterDef(factory = LowerCaseFilterFactory.class), #TokenFilterDef(factory = SnowballPorterFilterFactory.class, params = {#Parameter(name="language", value="Portuguese")} ) } ) }) public class NewsHeader implements Serializable { static final long serialVersionUID = 20140301L; private int id; private String articleHeader; private String language; private Set<NewsParagraph> paragraphs = new HashSet<NewsParagraph>(); /** * #return the id */ #Id #Column(name="ID") #GeneratedValue(strategy=GenerationType.AUTO) #DocumentId public int getId() { return id; } /** * #param id the id to set */ public void setId(int id) { this.id = id; } /** * #return the articleHeader */ #Column(name="ARTICLE_HEADER") #Field(index=Index.YES, store=Store.NO) public String getArticleHeader() { return articleHeader; } /** * #param articleHeader the articleHeader to set */ public void setArticleHeader(String articleHeader) { this.articleHeader = articleHeader; } /** * #return the language */ #Column(name="LANGUAGE") #Field #AnalyzerDiscriminator(impl=LanguageDiscriminator.class) public String getLanguage() { return language; } ... } This is my LanguageDiscriminator class public class LanguageDiscriminator implements Discriminator { #Override public String getAnalyzerDefinitionName(Object value, Object entity, String field) { String result = null; if (value != null) { result = (String) value; } return result; } } This is my search method present in my SearchDAO public List<NewsHeader> searchParagraph(String patternStr) { Session session = null; Transaction tx; List<NewsHeader> result = null; try { session = sessionFactory.getCurrentSession(); FullTextSession fullTextSession = Search.getFullTextSession(session); tx = fullTextSession.beginTransaction(); // Create native Lucene query using the query DSL QueryBuilder queryBuilder = fullTextSession.getSearchFactory() .buildQueryBuilder().forEntity(NewsHeader.class).get(); org.apache.lucene.search.Query luceneSearchQuery = queryBuilder .keyword() .onFields("articleHeader", "paragraphs.content") .matching(patternStr) .createQuery(); // Wrap Lucene query in a org.hibernate.Query org.hibernate.Query hibernateQuery = fullTextSession.createFullTextQuery(luceneSearchQuery, NewsHeader.class, NewsParagraph.class); // Execute search result = hibernateQuery.list(); } catch (Exception xcp) { logger.error(xcp); } finally { if ((session != null) && (session.isOpen())) { session.close(); } } return result; }
When creating a new record for the entity in the database, I can see that the "getAnalyzerDefinitionName()" method from the "LanguageDiscriminator" gets called. Great. But the same does not happen when I execute a search. Can anyone explain me why? The selection of the analyzer is dependent on the state of a given entity, in your case NewsHeader. You are dealing with entity instances during indexing. While querying you don't have entities to start with, you are searching for them. Which analyzer would you Hibernate Search to select for your query? That said, I think there is a shortcoming in the DSL. It does not allow you to explicitly specify the analyzer for a class. There is ignoreAnalyzer, but that's not what you want. I guess you could create a feature request in the Search issue tracker - https://hibernate.atlassian.net/browse/HSEARCH. In the mean time you can build the query using the native Lucene query API. However, you will need to know which language you are targeting with your query (for example via the preferred language of the logged in user or whatever). This will depend on your use case. It might be you are looking at the wrong feature to start with.