Запрос объекта гибернации для поиска самой последней полууникальной строки в одной таблице - PullRequest
0 голосов
/ 12 сентября 2018

У меня есть база данных 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-фу не мои сильные стороны.

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

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

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 на то, что я хочу эти столбцы в качестве результата / возвращаемого значения, когда я хочу просто ограничить возвращаемые объекты модели на основе уникальных значений столбцов.

Ответы [ 9 ]

0 голосов
/ 19 сентября 2018

Решение - сначала получить группы productName, purchaserName и max (purchaseDate) по productName, purchaserName с использованием отдельных критериев.В результате мы идентифицируем уникальную строку, используя эти три атрибута.Но здесь есть одна загвоздка, если один и тот же покупатель покупал один и тот же продукт более одного раза в один и тот же день, тогда мы не сможем идентифицировать уникальную строку, используя указанное выше условие, и это приведет к тому, что из БД будет получено несколько записей.Чтобы решить эту проблему, вам нужно использовать тип datetime или timestamp для поля 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)
0 голосов
/ 24 сентября 2018
List<Purchase> findByProductNameInAndPurchaserNameInAndPurchaseDateBefore(List<String> productNames, List<String> purchaserNames, Date before);

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

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

0 голосов
/ 18 сентября 2018

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

-- 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 должен быть довольно простым, и этого может быть достаточно для вас. Прошло много времени с тех пор, как я использовал Критерии гибернации (сейчас вы склонны использовать 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

UPDATE:

Я вижу, что 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

, который читается немного яснее.

0 голосов
/ 18 сентября 2018

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

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)
0 голосов
/ 18 сентября 2018

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

    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
0 голосов
/ 17 сентября 2018

Здравствуйте, я бы предложил вам одно очень простое решение на основе 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

Получив идентификаторы записей, вы можете выбрать Фактические продукты по идентификатору.

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

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

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

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

0 голосов
/ 17 сентября 2018

Обновление

Чтобы использовать Критерии гибернации, вы можете попробовать подход подзапроса:

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 дает вам существенное преимущество в производительности по сравнению с подходом подзапроса.

Однако, похоже, он не может быть переведен в Критерии гибернации (поскольку критерии требуют пути / отображения между сущностями)

0 голосов
/ 17 сентября 2018

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

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();

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

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

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

0 голосов
/ 12 сентября 2018

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...