Статьи

NoSQL с Hibernate OGM – Часть вторая: запрос ваших данных

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

  • использование языка запросов сохраняемости Java (JP-QL)
  • используя родной язык запросов NoSQL для хранилища данных по вашему выбору (если оно есть)
  • использование поисковых запросов Hibernate — в основном полнотекстовые запросы

Все эти альтернативы позволят вам выполнить запрос к хранилищу данных и получить результат в виде списка управляемых объектов.

Подготовка тестового класса

Мы собираемся добавить новый класс HikeQueryTest. Он заполнит хранилище данных информацией о походах:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class HikeQueryTest {
 
    private static EntityManagerFactory entityManagerFactory;
 
    @BeforeClass
    public static void setUpEntityManagerFactoryAndPopulateTheDatastore() {
        entityManagerFactory = Persistence.createEntityManagerFactory( "hikePu" );
 
            EntityManager entityManager = entityManagerFactory.createEntityManager();
 
            entityManager.getTransaction().begin();
 
            // create a Person
            Person bob = new Person( "Bob", "McRobb" );
 
            // and two hikes
            Hike cornwall = new Hike(
                "Visiting Land's End", new Date(), new BigDecimal( "5.5" ),
                new HikeSection( "Penzance", "Mousehole" ),
                new HikeSection( "Mousehole", "St. Levan" ),
                new HikeSection( "St. Levan", "Land's End" )
            );
            Hike isleOfWight = new Hike(
                "Exploring Carisbrooke Castle", new Date(), new BigDecimal( "7.5" ),
                new HikeSection( "Freshwater", "Calbourne" ),
                new HikeSection( "Calbourne", "Carisbrooke Castle" )
            );
 
            // let Bob organize the two hikes
            cornwall.setOrganizer( bob );
            bob.getOrganizedHikes().add( cornwall );
 
            isleOfWight.setOrganizer( bob );
            bob.getOrganizedHikes().add( isleOfWight );
 
            // persist organizer (will be cascaded to hikes)
            entityManager.persist( bob );
 
            entityManager.getTransaction().commit();
           entityManager.close();
    }
 
    @AfterClass
    public static void closeEntityManagerFactory() {
        entityManagerFactory.close();
    }
}

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

Теперь, когда у нас есть некоторые данные, мы можем начать писать тесты для их поиска.

Использование языка запросов сохраняемости Java (JP-QL)

JP-QL — это язык запросов, определенный как часть спецификации Java Persistence API (JPA) . Он предназначен для работы с объектами и быть независимым от базы данных.

Взяв сущность Hike в качестве примера:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
public class Hike {
 
    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;
 
    private String description;
    private Date date;
    private BigDecimal difficulty;
 
    @ManyToOne
    private Person organizer;
 
    @ElementCollection
    @OrderColumn(name = "sectionNo")
    private List<HikeSection> sections;
        
      // constructors, getters, setters, ...
}

Запрос JP-QL для получения списка доступных походов, упорядоченных по сложности, выглядит следующим образом:

1
SELECT h FROM Hike h ORDER BY h.difficulty ASC

Hibernate OGM проанализирует этот запрос и преобразует его в эквивалентный на родном языке запросов выбранного вами хранилища данных. Например, в Neo4j он создает и выполняет запрос Cypher, как показано ниже:

1
MATCH (h:Hike) RETURN h ORDER BY h.difficulty

В MongoDB, используя JavaScript API MongoDB в качестве нотации запроса, это выглядит так:

1
db.Hike.find({}, { "difficulty": 1})

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

Теперь, когда у вас есть понимание того, что происходит, мы можем начать запрашивать данные, которые мы сохранили. Мы можем, например, получить список доступных походов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Test
    public void canSearchUsingJPQLQuery() {
        // Get a new entityManager
        EntityManager entityManager = entityManagerFactory.createEntityManager();
 
        // Start transaction
        entityManager.getTransaction().begin();
 
        // Find all the available hikes ordered by difficulty
        List<Hike> hikes = entityManager
            .createQuery( "SELECT h FROM Hike h ORDER BY h.difficulty ASC" , Hike.class )
            .getResultList();
 
        assertThat( hikes.size() ).isEqualTo( 2 );
        assertThat( hikes ).onProperty( "description" ).containsExactly( "Visiting Land's End", "Exploring Carisbrooke Castle" );
 
        entityManager.getTransaction().commit();
        entityManager.close();
    }

Если вы использовали спецификацию JPA до того, как найдете этот код очень знакомым: это тот же код, который вы написали бы при работе с реляционной базой данных с использованием JPA.

Вы можете проверить это, переключив конфигурацию и зависимость между Neo4j и MongoDB: тест все равно пройдет без каких-либо изменений в коде.

Круто то, что вы можете использовать запросы JP-QL с хранилищами данных, у которых нет собственного механизма запросов. Парсер запросов Hibernate OGM в этом случае будет создавать полнотекстовые запросы, которые выполняются через Hibernate Search и Lucene. Позже мы увидим, как вы можете сделать это более подробно.

Результатом запроса является список управляемых объектов. Это означает, что изменения объектов будут применены к данным в базе данных автоматически. Вы также можете перемещаться по результирующему графу объектов, заставляя ленивые ассоциации загружаться по мере необходимости

Поддержка языка JP-QL не завершена и может меняться в зависимости от серверной части. Мы оставим детали в официальной документации Hibernate OGM. На данный момент поддерживается:

  • простые сравнения
  • NULL и не NULL
  • логические операторы И , ИЛИ , НЕ
  • Как и между
  • СОРТИРОВАТЬ ПО

Если JP-QL не подходит для вашего варианта использования, мы увидим, как вы можете выполнить запрос, используя родной язык бэкэнда по вашему выбору.

Использование родного языка запросов

Иногда вы можете решить пожертвовать переносимостью в пользу мощи основного языка запросов. Например, вы можете воспользоваться возможностями языка Cypher Neo4j для выполнения иерархических / рекурсивных запросов. Используя MongoDB, давайте пройдем через «Пензанс»:

1
2
// Search for the hikes with a section that start from "Penzace" in MongoDB
List<Hike> hikes = entityManager.createNativeQuery("{ $query : { sections : { $elemMatch : { start: 'Penzance' } } } }", Hike.class ).getResultList();

Тот же код с Neo4j будет выглядеть так:

1
2
3
// Search for the hikes with a section that start from "Penzace" in Neo4j
List<Hike> hikes = entityManager.createNativeQuery( "MATCH (h:Hike) -- (:Hike_sections {start: 'Penzance'} ) RETURN h",
Hike.class ).getResultList();

Важно отметить, что, как и запросы JPA, объекты, возвращаемые запросом, являются управляемыми объектами.

Вы также можете определить запросы, используя аннотацию javax.persistence.NamedNativeQuery:

1
2
3
4
5
@Entity
@NamedNativeQuery(
name = "PenzanceHikes",
query = "{ $query : { sections : { $elemMatch : { start: 'Penzance' } } } }", resultClass = Hike.class )
public class Hike { ... }

и затем выполните это так:

1
List<Hike> hikes = entityManager.createNamedQuery( "PenzanceHikes" ).getResultList();

Использование поисковых запросов Hibernate

Hibernate Search предлагает способ индексирования объектов Java в индексы Lucene и выполнения полнотекстовых запросов к ним. Индексы живут за пределами вашего хранилища данных. Это означает, что у вас могут быть возможности запросов, даже если они не поддерживаются изначально. Он также предлагает несколько интересных свойств с точки зрения набора функций и масштабируемости. В частности, используя Hibernate Search, вы можете разгрузить выполнение запроса, чтобы отделить узлы и масштабировать их независимо от фактических узлов хранилища данных.

Для этого примера мы будем использовать MongoDB. Сначала вам нужно добавить Hibernate Search в ваше приложение. В проекте Maven вам нужно добавить следующую зависимость в pom.xml:

1
2
3
4
5
6
7
8
<dependencies>
    ...
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-search-orm</artifactId>
    </dependency>
    ...
</dependencies>

Теперь вы можете выбрать, что вы хотите индексировать:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Entity
@Indexed
public class Hike {
 
    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;
 
    @Field
    private String description;
 
    private Date date;
    private BigDecimal difficulty;
 
    @ManyToOne
    private Person organizer;
 
    @ElementCollection
    @OrderColumn(name = "sectionNo")
    private List<HikeSection> sections;
        
    // constructors, getters, setters, ...
}

Аннотация @Indexed идентифицирует классы, которые мы хотим проиндексировать, а аннотация @Field указывает, какие свойства класса мы хотим индексировать. Каждый раз, когда новая сущность Hike сохраняется с помощью диспетчера сущностей, использующего Hibernate OGM, Hibernate Search автоматически добавляет его в индекс и отслеживает изменения в управляемых сущностях. Таким образом, индекс и хранилище данных обновляются.

Теперь вы можете искать походы в Карисбрук, используя запросы Lucene. В этом примере мы будем использовать построитель запросов, предоставляемый Hibernate Search:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void canSearchUsingFullTextQuery() {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
 
    entityManager.getTransaction().begin();
 
    //Add full-text superpowers to any EntityManager:
    FullTextEntityManager ftem = Search.getFullTextEntityManager(entityManager);
 
    // Optionally use the QueryBuilder to simplify Query definition:
    QueryBuilder b = ftem.getSearchFactory().buildQueryBuilder().forEntity( Hike.class ).get();
 
    // A Lucene query to search for hikes to the Carisbrooke castle:
    Query lq = b.keyword().onField("description").matching("Carisbrooke castle").createQuery();
 
    //Transform the Lucene Query in a JPA Query:
    FullTextQuery ftQuery = ftem.createFullTextQuery(lq, Hike.class);
 
    //This is a requirement when using Hibernate OGM instead of ORM:
    ftQuery.initializeObjectsWith( ObjectLookupMethod.SKIP, DatabaseRetrievalMethod.FIND_BY_ID );
 
    // List matching hikes
    List<Hike> hikes = ftQuery.getResultList();
    assertThat( hikes ).onProperty( "description" ).containsOnly( "Exploring Carisbrooke Castle" );
 
    entityManager.getTransaction().commit();
    entityManager.close();
}

Результатом кода будет список походов с упоминанием «Замок Карисбрук» в описании.

Hibernate Search — это очень мощный инструмент с множеством различных опций, и его описание в этом руководстве займет слишком много времени. Вы можете проверить справочную документацию, чтобы узнать больше об этом.

Заворачивать

Это все на данный момент. Как вы уже видели, Hibernate OGM предоставляет вам ряд опций для выполнения запросов к вашему хранилищу данных, которые должны покрывать большинство ваших типичных запросов: JP-QL, собственные запросы NoSQL и полнотекстовые запросы через Hibernate Search / Apache Lucene. Даже если вы никогда раньше не работали с хранилищами данных NoSQL, вы сможете легко поэкспериментировать с ними.

Вы можете найти полный пример кода этого поста (и предыдущего) на GitHub. Просто раскошелитесь и играйте как хотите.

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

Мы стремимся узнать ваше мнение, не стесняйтесь комментировать или связаться с нами , мы ответим на ваши вопросы и услышим ваши отзывы.

Спасибо Гуннару Морлингу ( @gunnarmorling ) и Давиде Д’Альто (@Github: DavidD ) за создание этого урока.