Запрос API динамических критериев JPA

Интересно, есть ли общий способ использования api критериев в сочетании с немного более сложной моделью?

У меня есть класс сущности, который имеет взаимно однозначные отношения с другими сущностями. Моя служебная оболочка, которая выполняет запрос к базе данных через api критериев, получает параметры из внешнего интерфейса для определения разбивки на страницы, сортировки и фильтрации.

Сущности

@Entity
public class Person implements Serializable {
    @Id
    private Long id;
    private String name;
    private String givenName;

    @Temporal(TemporalType.DATE)
    private Date birthdate;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "INFORMATION_ID")
    private Information information;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "ADDRESS_ID")
    private Address address;
    ...
}
@Entity
public class Information implements Serializable {
    @Id
    private Long id;
    private String detail;
    ...
}
@Entity
public class Address implements Serializable {
    @Id
    private Long id;
    private String street;
    private String city;
    ...
}

Услуга

@Stateless
public class PersonService {
    @PersistenceContext(unitName = "ProblemGenericDatatableFilterPU")
    private EntityManager em;
    public List<Person> findAllPersons222(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters) {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Person> criteriaQuery = builder.createQuery(Person.class);
        Root<Person> rootPerson = criteriaQuery.from(Person.class);
        Join<Person, Information> joinPersonInformation = rootPerson.join(Person_.information);
        Join<Person, Address> joinPersonAddress = rootPerson.join(Person_.address);

        // select
        criteriaQuery.select(rootPerson);

        // filter
        List<Predicate> allPredicates = new ArrayList<>();
        for(Entry<String, Object> currentEntry : filters.entrySet()) {
            Predicate currentPredicate;

            if(currentEntry.getKey().startsWith("information_")) {
                currentPredicate = builder.like(
                        builder.lower(joinPersonInformation.<String>get(currentEntry.getKey())),
                    builder.lower(builder.literal(String.valueOf(currentEntry.getValue())))
                );
            }
            else if(currentEntry.getKey().startsWith("address_")) {
                currentPredicate = builder.like(
                        builder.lower(joinPersonAddress.<String>get(currentEntry.getKey())),
                    builder.lower(builder.literal(String.valueOf(currentEntry.getValue())))
                );
            }
            else {
                currentPredicate = builder.like(
                        builder.lower(rootPerson.<String>get(currentEntry.getKey())),
                    builder.lower(builder.literal(String.valueOf(currentEntry.getValue())))
                );
            }
            allPredicates.add(currentPredicate);
        }
        criteriaQuery.where(builder.and(allPredicates.toArray(new Predicate[0])));

        // order
        if(sortField != null && !sortField.isEmpty()) {
            Order orderBy;
            if(sortField.startsWith("information_")) {
                orderBy = (sortOrder == SortOrder.DESCENDING
                        ? builder.desc(joinPersonInformation.get(sortField))
                        : builder.asc(joinPersonInformation.get(sortField)));
            }
            else if(sortField.startsWith("address_")) {
                orderBy = (sortOrder == SortOrder.DESCENDING
                        ? builder.desc(joinPersonAddress.get(sortField))
                        : builder.asc(joinPersonAddress.get(sortField)));
            }
            else {
                orderBy = (sortOrder == SortOrder.DESCENDING
                        ? builder.desc(rootPerson.get(sortField))
                        : builder.asc(rootPerson.get(sortField)));
            }
            criteriaQuery.orderBy(orderBy);
        }

        Query query = em.createQuery(criteriaQuery);
        // pagination
        query.setFirstResult(first);
        query.setMaxResults(pageSize);
        return query.getResultList();
    }
}

Мне нужно сделать различие случаев для фильтрации и сортировки в зависимости от корня / соединения, по которому я обращаюсь к свойству. Кроме того, мне нужно использовать соглашение об именах в фейсплете. То же самое касается запроса count, за исключением сортировки.

Теперь я спрашиваю себя, есть ли какие-нибудь «точечные обозначения» или что-то еще, что делает случай необязательным. В e. грамм. собственный SQL. Я бы сделал что-то вроде подзапроса и выбрал бы все значения псевдонимов из внутренней проекции (select * from (select person.name as name, address.street as street, ...) where name = ... and street like ...).

Буду признателен за любой совет.


person Filou    schedule 31.07.2015    source источник
comment
Вы можете проверить stackoverflow.com/a/31703382/381897   -  person bhdrkn    schedule 31.07.2015
comment
Привет @bhdrkn, спасибо за подсказку. Мне нравится ваш SimpleSelectBuilder, и я посмотрю на него поближе. Но боюсь, это не решит мою проблему. У вас есть только одно поле root, которое используется каждым предикатом. С моей проблемой отношений мне нужно построить предикаты для Root и различных динамических Join объектов, которые определяются именем поля.   -  person Filou    schedule 31.07.2015
comment
Привет, Filou, в SimpleSelectBuilder есть метод, поддерживающий глубокие соединения. Но он не поддерживает внутренний выбор. Может быть, вы можете добавить их для собственного использования.   -  person bhdrkn    schedule 31.07.2015
comment
Привет, bhdrkn, я более подробно изучил эти методы. Они действительно экономят много шаблонов, но, в конце концов, при использовании SimpleSelectBuilder вам нужно иметь метаинформацию, какое поле определено в каком соединении. Вы не можете перебирать параметр-Map<String, Object> и обрабатывать каждую строку одинаково. Тем не менее, мне все еще нравится ваш Builder :-) и, наверное, напишу что-нибудь под мои нужды. Большое спасибо!   -  person Filou    schedule 31.07.2015
comment
Если вы пишете что-то индивидуальное, пожалуйста, опубликуйте это как ответ   -  person Kukeltje    schedule 31.07.2015


Ответы (1)


Наконец-то у меня появилось время разобраться со своей проблемой. Я нашел решение, которое не идеально, но мне подходит.

Когда я искал другую проблему, я пришел к в этой статье Леонардо Шикиды и нашел очень удобный Path<?> getPath(...) метод (я также глубже изучил блестящие отношения наследования в CriteriaAPI: Path, Root, Join, Из и т. Д.). Имея это в виду, я вспомнил свою прежнюю проблему и подумал о более общем способе этого метода. Итак, вот что я сделал из этого:

Сначала я создаю все нужные мне соединения (например, Root<?> и Join<? ?>) и помещаю их в Map<String, From<?, ?>>, где String - это элемент, атрибут которого запрашивается в точечной нотации (соглашение об именах и обратная сторона полного решения) и From - соответствующий источник.

С помощью карты я могу выполнять фильтрацию и сортировку более или менее общим способом.

Чтобы заставить его работать, внешний интерфейс должен использовать то же самое соглашение об именах и соответствующим образом передать filter-Map (т. Е. JSF с использованием атрибута primefaces field в столбце p:).

public List<Person> newFindAllPersons(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters)
{
    CriteriaBuilder builder = em.getCriteriaBuilder();
    CriteriaQuery<Person> criteriaQuery = builder.createQuery(Person.class);

    // setting up the required joins
    Root<Person> rootPerson = criteriaQuery.from(Person.class);
    Join<Person, Information> joinPersonInformation = rootPerson.join(Person_.information);
    Join<Person, Address> joinPersonAddress = rootPerson.join(Person_.address);
    Join<Address, Information> joinAddressInformation = joinPersonAddress.join(Address_.information);

    // putting all joins into a map with a dot`ted name
    Map<String, From<?, ?>> mapFieldToFrom = new HashMap<>();
    mapFieldToFrom.put("person", rootPerson);
    mapFieldToFrom.put("person.address", joinPersonAddress);
    mapFieldToFrom.put("person.information", joinPersonInformation);
    mapFieldToFrom.put("person.address.information", joinAddressInformation);

    // select
    criteriaQuery.select(rootPerson);

    // filter
    List<Predicate> allPredicates = new ArrayList<>();
    for(Entry<String, Object> currentEntry : filters.entrySet())
    {
        Predicate currentPredicate = builder.like(
                builder.lower(getStringPath(currentEntry.getKey(), mapFieldToFrom)),
                builder.lower(builder.literal("%" + String.valueOf(currentEntry.getValue()) + "%"))
        );
        allPredicates.add(currentPredicate);
    }
    criteriaQuery.where(builder.and(allPredicates.toArray(new Predicate[0])));

    // order
    if(sortField != null && !sortField.isEmpty())
    {
        Path<?> actualPath = getStringPath(sortField, mapFieldToFrom);
        Order orderBy = (sortOrder == SortOrder.DESCENDING
                ? builder.desc(actualPath)
                : builder.asc(actualPath));

        criteriaQuery.orderBy(orderBy);
    }

    Query query = em.createQuery(criteriaQuery);
    // pagination
    query.setFirstResult(first);
    query.setMaxResults(pageSize);
    return query.getResultList();
}

/**
 * divides the given field at the last dot and takes <br>
 * -   the first part as the key in the map to retrieve the From<?, ?> <br>
 * -   the last part as the name of the column in the entity
 */
private Path<String> getStringPath(String field, Map<String, From<?, ?>> mapFieldToFrom)
{
    if(!field.matches(".+\\..+"))
    {
        throw new IllegalArgumentException("field '" + field + "' needs to be a dotted path (i. e. customer.address.city.zipcode)");
    }
    String fromPart = field.substring(0, field.lastIndexOf('.'));
    String fieldPart = field.substring(field.lastIndexOf('.') + 1);

    From<?, ?> actualFrom = mapFieldToFrom.get(fromPart);
    if(actualFrom == null)
    {
        throw new IllegalStateException("the given map does not contain a from or for the value '" + fromPart + "' or is null");
    }
    return actualFrom.get(fieldPart);
}

Пример внешнего интерфейса

<p:dataTable>
    <!-- mapFieldToFrom.put("person", rootPerson); -->
    <p:column field="person.name"> 
    </p:column>

    <!-- mapFieldToFrom.put("person.address", joinPersonAddress); -->
    <p:column field="person.address.street">
    </p:column>
</p:dataTable>
person Filou    schedule 04.12.2015