подобный оператор вызывает неподдерживаемый запрос в Ignite 2.7 с помощью QueryDSL

#java #spring-boot #sql-like #ignite #querydsl

#java #весенняя загрузка #sql-подобный #ignite #querydsl

Вопрос:

В приложении spring boot мы используем QueryDSL для доступа к базе данных. Приложение должно распечатать все элементы из таблицы, которые соответствуют параметрам поиска (зависящим от пользовательского ввода).

Примеры:

  • Дайте мне все местоположения с именем, которое точно соответствует «Berlin»
  • Дайте мне все местоположения с именем, начинающимся с «Ber».

Поэтому мы должны динамически создавать предложение where.

Мы используем класс сущностей, подобный

 package example;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "location")
public class LocationEntity {

    @Id
    @GeneratedValue
    @Column(name = "id", nullable = false)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    public LocationEntity() {
        // -
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  

класс запроса, подобный

 package example;

import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.StringPath;

public class QLocationEntity extends EntityPathBase<LocationEntity> {
    private static final long serialVersionUID = 1L;

    public static final QLocationEntity DEFAULT = new QLocationEntity("loc_1");

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public final StringPath name = createString("name");

    public QLocationEntity(String tableAlias) {
        super(LocationEntity.class, tableAlias);
    }
}
  

класс репозитория, подобный

 package example;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface LocationRepository extends JpaRepository<LocationEntity, Long>,
        /* needed for query DSL. */
        QuerydslPredicateExecutor<LocationEntity> {
    /*
     * We don't need custom methods.
     */
}
  

и небольшого вспомогательного класса, такого как

 package example;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.BooleanExpression;

@Service
public class LocationRepositoryHelper {
    protected final Log LOG = LogFactory.getLog(getClass());

    @Autowired
    private LocationRepository repository;

    public Sort sort = new Sort(Sort.Direction.ASC, "name");

    public Iterable<LocationEntity> findEntities(String paramLocation, boolean autocomplete)
            throws RequestParameterInvalideException {
        Predicate p = generatePredicate(paramLocation, autocomplete);
        if (p != null) {
            return repository.findAll(p, sort);
        } else {
            return repository.findAll(sort);
        }
    }

    BooleanExpression generatePredicate(String location, boolean autocomplete) {
        if ("".equals(location.trim())) {
            return null;
        } else if (autocomplete) {
            return QLocationEntity.DEFAULT.name.startsWith(location);
        } else {
            return QLocationEntity.DEFAULT.name.eq(location);
        }
    }
}
  

(This example is far less complex than our real application code, but it should be sufficient to demonstrate our problem.)

Our application asks the helper class for a list and spring boot should do the rest by voodoo magic.

When we use an oracle database as rdbms all is fine. When we use ignite, all is fine as long as we don’t try to use ‘autocompletion’.

When we try to «autocomplete the search string» we get an IgniteException that says: unsupported query: locationen0_.name like ?1 ESCAPE ‘!’

We logged the following sql string:

 select locationen0_.id as id1_0_, locationen0_.name as name2_0_ from my_location locationen0_ where locationen0_.name like ? escape '!' order by locationen0_.name asc
  

Мы предполагаем, что «?» должен быть заменен на «Ber%», поэтому полный оператор sql должен быть:

 select locationen0_.id as id1_0_, locationen0_.name as name2_0_ from my_location locationen0_ where locationen0_.name like 'Ber%' escape '!' order by locationen0_.name asc;
  

Мы запустили этот оператор вручную в консоли SQL в Oracle DB (12.*) и в Ignite (2.7). В Oracle все было в порядке, Ignite по-прежнему объявлял, что у нас будет синтаксическая ошибка / неподдерживаемый запрос.
Итак, мы попробовали несколько альтернатив в Ignite…

 select locationen0_.id as id1_0_, locationen0_.name as name2_0_ from my_location locationen0_ where locationen0_.name = 'Berlin' order by locationen0_.name asc;
=> all fine, but doesn't return what we want.

select locationen0_.id as id1_0_, locationen0_.name as name2_0_ from my_location locationen0_ where locationen0_.name like 'Berlin' order by locationen0_.name asc;
=> all fine, but still doesn't return what we want.

select locationen0_.id as id1_0_, locationen0_.name as name2_0_ from my_location locationen0_ where locationen0_.name = 'Ber%' order by locationen0_.name asc;
=> still all fine and would return what we want.

select locationen0_.id as id1_0_, locationen0_.name as name2_0_ from my_location locationen0_ where locationen0_.name = 'Berlin' escape '!' order by locationen0_.name asc;
=> all fine (woot??), but wouldn't return what we want.

select locationen0_.id as id1_0_, locationen0_.name as name2_0_ from my_location locationen0_ where locationen0_.name = 'Ber%' escape '!' order by locationen0_.name asc;
=> (the original statement) unsupported query, but is what queryDSL (supposedly) generates and what should return what we want.
  

Наш первый вывод таков:

  1. Ignite знает ключевое слово ESCAPE; использование ключевого слова не вызывает проблем во всех случаях.
  2. Ignite понимает подобные операторы с «%» внутри.
  3. Ignite не принимает подобные операторы с «%» внутри в сочетании с ключевым словом escape.

После нескольких часов анализа мы теперь знаем, «что вызывает проблему», но мы не знаем, почему это вообще проблема. Платформа QueryDSL (версия 4.2.1) жестко привязывает ключевое слово escape, поэтому мы понятия не имеем, как его подавить. Переключение на другую платформу может быть вариантом, хотя мы хотели бы избежать рефакторинга. Сброс фреймворка и «построение оператора с помощью конкатенации строк» сработали бы, но это не вариант для продуктивного кода.

Итак, наши вопросы таковы: кто-нибудь использует QueryDSL и Ignite и у него нет этой проблемы? Если да, используете ли вы QueryDSL совершенно иным способом, чем мы? (Используем ли мы QueryDSL таким образом, «что он не предназначен для использования»?) Или вы знаете параметр конфигурации для Ignite, который решает проблему? У кого-нибудь еще есть подсказка?

Ответ №1:

Я исследовал исходный код, и оказывается, что Ignite явно запрещает ESCAPE. Мы проверяем, указан ли ESCAPE, и указываем на ошибку, если это так.

Я думаю, вы можете направить проблему на Apache Ignite JIRA.