Запрос объекта гибернации для поиска последней, полу-уникальной строки в одной таблице

У меня есть база данных Hibernate с одной таблицей, которая выглядит так:

PURCHASE_ID | PRODUCT_NAME | PURCHASE_DATE | PURCHASER_NAME | PRODUCT_CATEGORY
------------------------------------------------------------------------------
     1          Notebook      09-07-2018          Bob            Supplies
     2          Notebook      09-06-2018          Bob            Supplies
     3           Pencil       09-06-2018          Bob            Supplies
     4            Tape        09-10-2018          Bob            Supplies
     5           Pencil       09-09-2018         Steve           Supplies
     6           Pencil       09-06-2018         Steve           Supplies
     7           Pencil       09-08-2018         Allen           Supplies

И я хочу вернуть только самые новые покупки, основанные на некоторых других ограничениях. Например:

List<Purchase> getNewestPurchasesFor(Array<String> productNames, Array<String> purchaserNames) { ... }

Можно вызвать, используя:

List<Purchase> purchases = getNewestPurchasesFor(["Notebook", "Pencil"], ["Bob", "Steve"]);

На английском языке: "Дайте мне новейшие покупки, будь то ноутбук или карандаш, либо Бобом, либо Стивом".

И обеспечит:

PURCHASE_ID | PRODUCT_NAME | PURCHASE_DATE | PURCHASER_NAME
-----------------------------------------------------------
     1          Notebook      09-07-2018          Bob            
     3           Pencil       09-06-2018          Bob            
     5           Pencil       09-09-2018         Steve           

Таким образом, это похоже на "отдельный" поиск по нескольким столбцам или "лимит" на основе некоторого постсортированного уникального ключа с комбинированными столбцами, но все примеры, которые я нашел, показывают, используя SELECT DISTINCT(PRODUCT_NAME, PURCHASER_NAME) чтобы получить эти только столбцы, тогда как мне нужно использовать формат:

from Purchases as entity where...

Чтобы типы моделей возвращались с неизменными отношениями.

В настоящее время мой запрос возвращает мне все старые покупки:

PURCHASE_ID | PRODUCT_NAME | PURCHASE_DATE | PURCHASER_NAME | PRODUCT_CATEGORY
------------------------------------------------------------------------------
     1          Notebook      09-07-2018          Bob            Supplies
     2          Notebook      09-06-2018          Bob            Supplies
     3           Pencil       09-06-2018          Bob            Supplies
     5           Pencil       09-09-2018         Steve           Supplies
     6           Pencil       09-06-2018         Steve           Supplies

Который, для повторных покупок, приводит к снижению производительности.

Существуют ли какие-либо специальные ключевые слова, которые я должен использовать для этого? Языки запросов и SQL-fu не мои сильные костюмы.

Редактировать:

Обратите внимание, что я в настоящее время использую API Criteria и хотел бы продолжать это делать.

Criteria criteria = session.createCriteria(Purchase.class);
criteria.addOrder(Order.desc("purchaseDate"));
// Product names
Criterion purchaseNameCriterion = Restrictions.or(productNames.stream().map(name -> Restrictions.eq("productName", name)).toArray(Criterion[]::new));
// Purchaser
Criterion purchaserCriterion = Restrictions.or(purchaserNames.stream().map(name -> Restrictions.eq("purchaser", name)).toArray(Criterion[]::new));
// Bundle the two together
criteria.add(Restrictions.and(purchaseNameCriterion, purchaserCriterion));

criteria.list(); // Gives the above results

Если я попытаюсь использовать отдельную проекцию, я получаю сообщение об ошибке:

ProjectionList projections = Projections.projectionList();
projections.add(Projections.property("productName"));
projections.add(Projections.property("purchaser"));
criteria.setProjection(Projections.distinct(projections));

Результаты в:

17:08:39 ERROR Order by expression "THIS_.PURCHASE_DATE" must be in the result list in this case; SQL statement:

Поскольку, как упоминалось выше, добавление проекционного/различного набора столбцов указывает на Hibernate, что я хочу, чтобы эти столбцы были как результат/возвращаемое значение, когда я хочу просто ограничить возвращаемые объекты модели на основе уникальных значений столбца.

Ответ 1

Во-первых, используйте запрос агрегации, чтобы получить дату последней покупки для комбинации продукт + покупатель.

Используйте этот запрос как подвыбор, соответствующий кортежам:

from Puchases p 
where (p.PRODUCT_NAME, p1.PURCHASER_NAME, p1.PURCHASE_DATE) in
    (select PRODUCT_NAME, PURCHASER_NAME , max(PURCHASE_DATE) 
     from Purchases 
     where 
        PRODUCT_NAME in :productNames and 
        PURCHASER_NAME in :purchaserNames 
     group by PRODUCT_NAME, PURCHASER_NAME)

Должна быть возможность реализовать то же самое с использованием API критериев, используя Subqueries.propertiesIn.

См. Запрос критериев гибернации для нескольких столбцов с предложением IN и вложенным выбором

.Если ваш PURCHASE_ID гарантированно является "хронологически возрастающим", тогда вы можете просто использовать max (PURCHASE_ID) в подвыборке.

Ответ 2

Вы можете создавать временные переменные и таблицы с помощью @. Я не уверен, как создать массив.

declare @product1 = 'Pencil'
declare @product2 = 'Notebook'
declare @purchaser_name1 = 'Bob'
declare @purchaser_name2= 'Steve'

Это должно получить самую последнюю дату покупки для каждой комбинации cust/prod

select 
product_name, purchaser_name, max(purchase_date) as max_purchase_date
into @temp
from purchases with(nolock) where 
product_name in (@product1,@product2) and
purchaser_name in (@purchaser_name1,@purchaser_name2)
group by product_name, purchaser_name

Если вам нужен круг назад и получить идентификатор, вы можете присоединиться к покупкам, чтобы получить их

select p.* from purchases p with(nolock) 
inner join @temp t 
on p.product_name = t.product_name
and p.purchaser_name = t.purchaser_name
and p.purchase_date = t.max_purchase_date

Обратите внимание на "с (nolock)" после имен таблиц. что может помочь в производительности.

Ответ 3

Хорошо, поскольку сначала я создал запрос, который извлекает только запрошенные записи:

select p1.* from Purchase p1

  join (
    select 
        max(PURCHASE_DATE) as maxdate, 
        purchaser_name, 
        PRODUCT_NAME from Purchase 
    where 
        (product_name ='Notebook' or product_name = 'Pencil') 
        and purchaser_name in ('Bob','Steve')
    group by 
        purchaser_name, 
        PRODUCT_NAME) p2

  on p1.PURCHASE_DATE = p2.maxDate
  and p1.PRODUCT_NAME = p2.PRODUCT_NAME
  and p1.PURCHASER_NAME = p2.PURCHASER_NAME;

который дал в качестве результата

PURCHASE_ID PRODUCT_NAME    PURCHASE_DATE             PURCHASER_NAME    PRODUCT_CATEGORY
1           Notebook        2018-07-09 00:00:00.000   Bob               Supplies
3           Pencil          2018-06-09 00:00:00.000   Bob               Supplies
5           Pencil          2018-09-09 00:00:00.000   Steve             Supplies

Теперь мы можем преобразовать этот запрос в SQLQuery и преобразовать его в bean-компонент с .setResultTransformer(Transformers.aliasToBean(Purchase.class)). Обратите внимание, что я назвал yourSession сеанс сеансом, изменив его следующим образом:

List<Purchase> list = yourSession.createSQLQuery(
          "select p1.* from Purchase p1 "
        + " join ( "
        + "     select "
        + "         max(PURCHASE_DATE) as maxdate, "
        + "         purchaser_name, "
        + "         PRODUCT_NAME from Purchase "
        + "     where "
        + "         (product_name ='Notebook' or product_name = 'Pencil') " //this must be created dinamically based on your parameters
        + "         and purchaser_name in ('Bob','Steve') " //and this too
        + "     group by "
        + "         purchaser_name, "
        + "         PRODUCT_NAME) p2 "

        + " on p1.PURCHASE_DATE = p2.maxDate "
        + " and p1.PRODUCT_NAME = p2.PRODUCT_NAME "
        + " and p1.PURCHASER_NAME = p2.PURCHASER_NAME ")
        .setResultTransformer(Transformers.aliasToBean(Purchase.class))
        .list();

Ofc, что сейчас отсутствует, это передать ваши параметры, такие как Notebook или Bob в методе, в который вы будете вставлять этот код. Я бы построил вспомогательный метод, который записывает условие на основе того, насколько велик список параметров.

Поскольку у меня нет спящего режима, связанного с базой данных, я освободил код, некоторые исправления могут потребоваться, но общая идея должна сделать трюк.

Выполнение этого без SQLQuery, imho, гораздо труднее и труднее читать: вам нужно сохранить целостность результата в вашем компоненте, и именно этого вы и достигнете.

Ответ 4

Обновить

Чтобы использовать критерии спящего режима, вы можете попробовать подзапрос:

DetachedCriteria subQuery = DetachedCriteria.forClass(Purchase.class, "p2");

ProjectionList groupBy = Projections.projectionList();
groupBy.add(Projections.max("purchaseDate"));
groupBy.add(Projections.groupProperty("productName"));
groupBy.add(Projections.groupProperty("purchaserName"));
subQuery.setProjection(groupBy);

subQuery.add(Restrictions.in("productName", productNames));
subQuery.add(Restrictions.in("purchaserName", purchaserName));

Criteria purchase = session.createCriteria(Purchase.class, "p1");
purchase.add(Subqueries.propertiesIn(new String[] {"purchaseDate", "productName", "purchaserName"}, subQuery));
purchase.addOrder(Order.desc("purchaseDate"));

List<Purchase> p1 = purchase.list();

Другой способ - использовать собственный SQL:

SELECT p1.*
FROM purchase p1 LEFT JOIN purchase p2
  ON (p1.purchaser_name = p2.purchaser_name 
      AND p1.product_name = p2.product_name 
      AND p1.purchase_date < p2.purchase_date)
WHERE p2.id IS NULL 
      AND p1.product_name IN ("Notebook", "Pencil") 
      AND p1.purchaser_name IN ("Bob", "Steve")
ORDER BY p1.product_name DESC

Этот SQL дает вам сильное преимущество в производительности по сравнению с подзапросом.

Однако он не переводится в Hibernate Criteria (поскольку для критериев требуется путь/сопоставление между сущностями)

Ответ 5

Привет, я предлагаю вам очень простое решение на основе HQL без слишком большого количества магии. Решением является следующий запрос HQL:

select p.id, max(p.date) from Purchase p where p.productName in('notebook','pencil') and p.purchaseName in ('ob', 'Steve') group by p.productName ,p.purchaseName

Когда у вас есть идентификаторы записей, вы можете выбрать Actual products by ID.

Теперь вы подумали бы, что этот запрос вернет вам всю таблицу. Не будет. Самая современная база данных, поддерживающая курсоры на стороне сервера, вернет вам только указанное количество записей, которые вы указали.

После инициализации запроса следующий шаг - это указать, сколько результатов вы хотите вернуть:

    Query query = query.setMaxResults(1)
    query.setFetchSize();
    query.scroll(ScrollMode.FORWARD_ONLY);
    // here is a hint for MySQL
    query.setMaxResults(100)

Это правильно использовало этот запрос, который не вернет вам полную таблицу! Он вернет столько, сколько сказано.

Ответ 6

Попробуйте использовать этот код.

    SessionFactory sessFact = HibernateUtil.getSessionFactory();
    Session session = sessFact.openSession();
    Criteria criteria = session.createCriteria(Purchase.class);
    ProjectionList projList = Projections.projectionList();

    projList.add(Projections.groupProperty("purchaserName"));
    projList.add(Projections.groupProperty("productName"));
    projList.add(Projections.property("purchaseId"));
    projList.add(Projections.property("productName"));
    projList.add(Projections.max("purchaseDate"));
    projList.add(Projections.property("purchaserName"));

    criteria.setProjection(projList);

    List<String> productList = new ArrayList<String>() {
        {
            add("Notebook");
            add("Pencil");
        }
    };
    List<String> purchaserList = new ArrayList<String>() {
        {
            add("Bob");
            add("Steve");
        }
    };

    Disjunction prod = Restrictions.disjunction();
    prod.add(Restrictions.in("productName", productList));

    Disjunction purch = Restrictions.disjunction();
    purch.add(Restrictions.in("purchaserName", purchaserList));

    criteria.add(Restrictions.and(prod, purch));
    List resultList = criteria.list();

SQL в результате (<property name="show_sql">true</property>)

select this_.PURCHASER_NAME as y0_, this_.PRODUCT_NAME as y1_, this_.PURCHASE_ID as y2_, this_.PRODUCT_NAME as y3_, max(this_.PURCHASE_DATE) as y4_, this_.PURCHASER_NAME as y5_ from purchase this_ where ((this_.PRODUCT_NAME in (?, ?)) and (this_.PURCHASER_NAME in (?, ?))) group by this_.PURCHASER_NAME, this_.PRODUCT_NAME

Ответ 7

На мой взгляд, хитрость заключается в том, чтобы увидеть, что "дать мне новейшую" эквивалентно "дать строки без новых покупок". Это переводит на этот вид запроса:

-- This is SQL
-- Note that if two purchases have exactly the same date, this query will
-- return both; you can fine tune the condition inside the exists clause
-- to avoid this
select *
from purchases p1
where
p1.product_name in ('Notebook', 'Pencil') and
p1.purchaser_name in ('Bob', 'Steve') and
not exists (
   select p2.purchase_id
   from purchases p2
   where
   p2.product_name = p1.product_name and
   p2.purchaser_name = p1.purchaser_name and
   p2.purchase_date > p1.purchase_date
)
order by purchase_id;

Хотя это SQL, перевод на HQL должен быть довольно простым, и этого может быть достаточно для вас. Прошло много времени с тех пор, как я использовал критерии Hibernate (в наши дни вы используете API JPA), но это должно быть нечто похожее на это:

DetachedCriteria criteria = DetachedCriteria.forClass(Purchase.class, "p1");
// add here your filters to criteria
// criteria.add(purcharserName in (....));
// criteria.add(productName in (....));
// this appends the not exists clause
DetachedCriteria notExistsCriteria = DetachedCriteria.forClass(Purchase.class, "p2");
notExistsCriteria.add(Restrictions.eqProperty("p2.productName", "p1.productName"));
notExistsCriteria.add(Restrictions.eqProperty("p2.purchaserName", "p1.purchaserName"));
notExistsCriteria.add(Restrictions.gtProperty("p2.purchaseDate", "p1.purchaseDate"));

criteria.add(Subqueries.notExists(notExistsCriteria.setProjection(Projections.property("p1.id"))));

List<Purchase> results = // issue Criteria query

ОБНОВИТЬ:

Я вижу, что Hibernate Criteria поддерживает оператор SQL ALL, поэтому, если ваша база данных поддерживает его, вы также можете написать следующее:

DetachedCriteria criteria = DetachedCriteria.forClass(Purchase.class, "p1");
// add here your filters to criteria
// criteria.add(purcharserName in (....));
// criteria.add(productName in (....));

// this appends the p1.purchaseDate > all (...) filter
DetachedCriteria allCriteria = DetachedCriteria.forClass(Purchase.class, "p2");
allCriteria.add(Restrictions.eqProperty("p2.productName", "p1.productName"));
allCriteria.add(Restrictions.eqProperty("p2.purchaserName", "p1.purchaserName"));

criteria.add(Subqueries.propertyGeAll("p1.purchaseDate", allCriteria.setProjection(Projections.property("p2.purchaseDate"))));

List<Purchase> results = // issue Criteria query

который читается несколько более четко.

Ответ 8

Попробуйте использовать следующий HQL, предполагая, что у вас есть поле auto incrementing id.

FROM Purchase p WHERE p.id IN(SELECT MAX(p1.id) FROM Purchase p1 WHERE p1.productName IN('Notebook','Pencil') AND p1.purchaseName IN('Bob', 'Steve') GROUP BY p1.productName, p1.purchaseName)

Ответ 9

Решение заключается в том, чтобы сначала получить имя productname, buyeraserName и max (buyDate) с помощью productName, имя покупателя, используя отдельные критерии. Это приведет к тому, что мы будем идентифицировать уникальную строку, используя эти три атрибута. Но здесь есть один улов, если один и тот же покупатель купил один и тот же продукт более одного раза в тот же день, тогда мы не сможем идентифицировать уникальную строку, используя вышеуказанное условие, и это приведет к появлению нескольких записей, полученных из БД. Чтобы решить эту проблему, вам необходимо использовать тип даты и времени для поля purchaseDate в БД. Теперь используйте эти атрибуты из отдельных критериев в запросе Criteria, чтобы получить требуемый результат.

DetachedCriteria detachedCriteria = DetachedCriteria.forClass(Purchase.class, "inner");
    detachedCriteria.add(Restrictions.in("inner.productName", new String[] { "Notebook", "Pencil" }));
    detachedCriteria.add(Restrictions.in("inner.purchaserName", new String[] { "Bob", "Steve" }));
    detachedCriteria.setProjection(Projections.projectionList().add(Projections.max("inner.purchaseDate"))
            .add(Projections.groupProperty("inner.productName"))
            .add(Projections.groupProperty("inner.purchaserName")));
    Session session = this.getEntityManager().unwrap(Session.class);
    Criteria criteria = session.createCriteria(Purchase.class, "b");
    ProjectionList projectionList = Projections.projectionList();
    projectionList.add(Projections.property("b.purchaseId"));
    projectionList.add(Projections.property("b.productName"));
    projectionList.add(Projections.property("b.purchaseDate"));
    projectionList.add(Projections.property("b.purchaserName"));
    criteria.setProjection(projectionList);
    criteria.add(Subqueries.propertiesIn(new String[] { "b.purchaseDate", "b.productName", "b.purchaserName" },
            detachedCriteria));
    criteria.list();

Этот запрос критериев будет срабатывать ниже запроса в mysql

select this_.purchase_id as y0_, this_.product_name as y1_, this_.purchase_date as y2_, this_.purchaser_name as y3_ from purchase this_ where (this_.purchase_date, this_.product_name, this_.purchaser_name) in (select max(inner_.purchase_date) as y0_, inner_.product_name as y1_, inner_.purchaser_name as y2_ from purchase inner_ where inner_.product_name in (?, ?) and inner_.purchaser_name in (?, ?) group by inner_.product_name, inner_.purchaser_name)

Ответ 10

List<Purchase> findByProductNameInAndPurchaserNameInAndPurchaseDateBefore(List<String> productNames, List<String> purchaserNames, Date before);

Я не вижу объект Purchase, но, конечно, если Product сопоставляется как сущность, этот запрос должен быть соответствующим образом обновлен.

ПРИМЕЧАНИЕ. Я бы предложил вам прочитать о полнотекстовом поиске или поиске Hibernate, потому что если у вас будет много запросов, подобных этому, похоже, вам понадобится поддержка полного текстового поиска в вашем проекте.