Статьи

MongoDB и Java Tutorial

Эта статья является частью нашего курса Академии под названием MongoDB — A Scalable NoSQL DB .

В этом курсе вы познакомитесь с MongoDB. Вы узнаете, как установить его и как управлять им через оболочку. Кроме того, вы узнаете, как получить программный доступ к нему через Java и как использовать Map Reduce с ним. Наконец, будут объяснены более сложные понятия, такие как шардинг и репликация. Проверьте это здесь !

1. Введение

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

В этой части мы рассмотрим интеграцию MongoDB с приложениями, написанными на Java. Наш выбор Java обусловлен его популярностью, но MongoDB предоставляет привязки (или драйверы) ко многим другим языкам, полный список которых можно найти в официальной документации .

Мы собираемся разработать простое приложение для книжного магазина с целью охватить большинство случаев использования, с которыми вы можете столкнуться, с упором на способы их решения MongoDB . Решения будут представлены в виде типичных тестовых примеров JUnit с удивительными плавными утверждениями, предоставляемыми AssertJ , время от времени сопровождаемыми командами оболочки MongoDB в качестве шагов проверки.

Хотя Spring Data MongoDB на сегодняшний день является самым популярным выбором в сообществе Java, мы будем использовать другую замечательную библиотеку под названием Morphia : легковесная библиотека с безопасным типом для отображения объектов Java в MongoDB и из нее . Последняя версия библиотеки Morphia на момент написания статьи — 0.106, и, к сожалению, она пока не поддерживает все функции MongoDB 2.6 (например, текстовые индексы и полнотекстовый поиск).

2. Расширения

Еще одна хорошая вещь о Morphia является его расширяемость. В частности, нас очень интересует расширение JSR 303: Bean Validation 1.0, предоставляемое командой Morphia из коробки. Это очень полезное дополнение, которое помогает обеспечить правильность и значимость объектов данных, проходящих через систему. Мы увидим пару примеров позже.

3. Зависимости

Наш проект будет использовать Apache Maven для управления сборкой и зависимостями, поскольку это довольно популярный выбор в сообществе Java. К счастью, релизы Morphia доступны через публичные репозитории Apache Maven, и мы собираемся использовать эти три модуля:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<properties>
    <org.mongodb.morphia.version>0.106</org.mongodb.morphia.version>
</properties>
 
<dependencies>
    <dependency>
        <groupId>org.mongodb.morphia</groupId>
        <artifactId>morphia</artifactId>
        <version>${org.mongodb.morphia.version}</version>
    </dependency>
 
    <dependency>
        <groupId>org.mongodb.morphia</groupId>
        <artifactId>morphia-validation</artifactId>
        <version>${org.mongodb.morphia.version}</version>
    </dependency>
         
    <dependency>
        <groupId>org.mongodb.morphia</groupId>     
        <artifactId>morphia-logging-slf4j</artifactId>
        <version>${org.mongodb.morphia.version}</version>
    </dependency>
</dependencies>

Основной модуль предоставляет аннотации, сопоставления и, в основном, все необходимые классы, чтобы начать создавать приложения и использовать MongoDB . Модуль проверки (или расширение) обеспечивает интеграцию с JSR 303: Bean Validation 1.0, как мы упоминали ранее. Наконец, модуль регистрации интегрируется с отличной платформой SLF4J .

4. Модель данных

Поскольку мы создаем простое приложение для книжного магазина , его область данных может быть представлена ​​этими тремя классами:

  • Автор : автор книги
  • Книга : сама книга
  • Магазин : магазин по продаже книг

Чтобы спроецировать то, что мы только что сказали, в терминологию MongoDB , мы собираемся создать книжный магазин базы данных с тремя коллекциями документов:

  • авторы : все известные авторы книг
  • книги : все доступные книги
  • магазины : все существующие магазины, продающие книги

Прозрачное отображение между классами Java ( Author, Book, Store ) и коллекциями MongoDB ( авторы, книги, магазины ) является одной из обязанностей Morphia . Давайте посмотрим на класс Authorfirst, так как он самый простой.

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.javacodegeeks.mongodb;
 
import org.bson.types.ObjectId;
import org.hibernate.validator.constraints.NotEmpty;
import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id;
import org.mongodb.morphia.annotations.Index;
import org.mongodb.morphia.annotations.Indexes;
import org.mongodb.morphia.annotations.Property;
import org.mongodb.morphia.annotations.Version;
 
@Entity( value = "authors", noClassnameStored = true )
@Indexes( {
    @Index( "-lastName, -firstName" )
} )
public class Author {
    @Id private ObjectId id;
    @Version private long version;
    @Property @NotEmpty private String firstName;
    @Property @NotEmpty private String lastName;
         
    public Author() {
    }
         
    public Author( final String firstName, final String lastName ) {
        this.lastName = lastName;
        this.firstName = firstName;
    }
 
    public ObjectId getId() {
        return id;
    }
     
    protected void setId( final ObjectId id ) {
        this.id = id;
    }
 
    public long getVersion() {
        return version;
    }
 
    public void setVersion( final long version ) {
        this.version = version;
    }
 
    public String getLastName() {
        return lastName;
    }
 
    public void setLastName( final String lastName ) {
        this.lastName = lastName;
    }
 
    public String getFirstName() {
        return firstName;
    }
 
    public void setFirstName( final String firstName ) {
        this.firstName = firstName;
    }
}

Для разработчиков Java, работающих с JPA и / или Hibernate , такие POJO (простые старые объекты Java) очень знакомы. Аннотация @Entity отображает класс Java в коллекцию MongoDB , в данном случае Автор -> авторы . noClassnameStored примечание о noClassnameStored : по умолчанию Morphia хранит полное имя класса Java внутри каждого документа MongoDB . Это на самом деле весьма полезно, если одна и та же коллекция может содержать документы разных типов (имеется в виду экземпляры разных классов Java). В нашем приложении мы не собираемся использовать такую ​​функцию, поэтому Morphia поручено не хранить эту информацию (что делает документы более чистыми).

Далее мы объявляем поля документа MongoDB как свойства класса Java. В случае Автора это включает в себя:

  • id (помеченный @Id)
  • версия (помеченная @Version), мы собираемся использовать это свойство для оптимистической блокировки в случае одновременных обновлений
  • firstName (помечено @Property), кроме того, определено ограничение проверки @NotEmpty, которое требует установки этого свойства
  • lastName (помечено @Property), кроме того, определено ограничение проверки @NotEmpty, которое требует установки этого свойства

И наконец, класс Author определяет один составной индекс для свойств lastName и firstName (концепции индексирования будут подробно рассмотрены в части 7. Руководство по безопасности, профилированию, индексированию, курсорам и массовым операциям MongoDB ).

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

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package com.javacodegeeks.mongodb;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
 
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
 
import org.bson.types.ObjectId;
import org.hibernate.validator.constraints.NotEmpty;
import org.mongodb.morphia.annotations.Embedded;
import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id;
import org.mongodb.morphia.annotations.Indexed;
import org.mongodb.morphia.annotations.Property;
import org.mongodb.morphia.annotations.Reference;
import org.mongodb.morphia.annotations.Version;
import org.mongodb.morphia.utils.IndexDirection;
 
@Entity( value = "books", noClassnameStored = true )
public class Book {
    @Id private ObjectId id;
    @Version private long version;   
    @Property @Indexed( IndexDirection.DESC ) @NotEmpty private String title;
    @Reference @Valid private List< Author > authors = new ArrayList<>();
    @Property( "published" ) @NotNull private Date publishedDate;           
    @Property( concreteClass = TreeSet.class ) @Indexed
    private Set< String > categories = new TreeSet<>();
    @Embedded @Valid @NotNull private Publisher publisher;
         
    public Book() {
    }
     
    public Book( final String title ) {
        this.title = title;
    }
     
    public ObjectId getId() {
        return id;
    }
     
    protected void setId( final ObjectId id ) {
        this.id = id;
    }
 
    public long getVersion() {
        return version;
    }
 
    public void setVersion( final long version ) {
        this.version = version;
    }
 
    public String getTitle() {
        return title;
    }
 
    public void setTitle( final String title ) {
        this.title = title;
    }
 
    public List< Author > getAuthors() {
        return authors;
    }
 
    public void setAuthors( final List< Author > authors ) {
        this.authors = authors;
    }
 
    public Date getPublishedDate() {
        return publishedDate;
    }
 
    public void setPublishedDate( final Date publishedDate ) {
        this.publishedDate = publishedDate;
    }
 
    public Set< String > getCategories() {
        return categories;
    }
 
    public void setCategories( final Set< String > categories ) {
        this.categories = categories;
    }
 
    public Publisher getPublisher() {
        return publisher;
    }
 
    public void setPublisher( final Publisher publisher ) {
        this.publisher = publisher;
   }
}

Большинство аннотаций мы уже видели, но есть пара новых:

  • ключ публикации (помечен как @Property с именем «опубликовано» в документе)
  • издатель (помеченный @Embedded, весь объект будет храниться внутри каждого документа), кроме того, он имеет определенное ограничение проверки @Validand @NotNull, которое требует, чтобы это свойство было установлено и было действительным (соответствует собственным ограничениям проверки)
  • авторы (помеченные @Reference, ссылки на авторов будут храниться внутри каждого документа), кроме того, для него определено ограничение проверки @Valid, которое требует, чтобы каждый автор в коллекции был действительным (соответствует собственным ограничениям проверки)

Свойства title и categories имеют собственные индексы, определенные с помощью аннотации @Indexed. Класс Publisher не аннотирован @Entity и поэтому не сопоставляется ни с одной коллекцией MongoDB, являющейся простым Java-бином. При этом класс Publisher все еще может объявлять индексы, которые будут частью коллекции MongoDB, в которую этот класс встраивается ( books ).

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
package com.javacodegeeks.mongodb;
 
import org.hibernate.validator.constraints.NotBlank;
import org.mongodb.morphia.annotations.Indexed;
import org.mongodb.morphia.annotations.Property;
import org.mongodb.morphia.utils.IndexDirection;
 
public class Publisher {
    @Property @Indexed( IndexDirection.DESC ) @NotBlank private String name;
     
    public Publisher() {
    }
     
    public Publisher( final String name ) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}

Наконец, давайте посмотрим на класс Store . Этот класс в основном склеивает все концепции, которые мы видели в классах Author и Book .

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.javacodegeeks.mongodb;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.validation.Valid;
 
import org.bson.types.ObjectId;
import org.hibernate.validator.constraints.NotEmpty;
import org.mongodb.morphia.annotations.Embedded;
import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id;
import org.mongodb.morphia.annotations.Indexed;
import org.mongodb.morphia.annotations.Property;
import org.mongodb.morphia.annotations.Version;
import org.mongodb.morphia.utils.IndexDirection;
 
@Entity( value = "stores", noClassnameStored = true )
public class Store {
    @Id private ObjectId id;
    @Version private long version;
    @Property @Indexed( IndexDirection.DESC ) @NotEmpty private String name;
    @Embedded @Valid private List< Stock > stock = new ArrayList<>();
    @Embedded @Indexed( IndexDirection.GEO2D ) private Location location;
     
    public Store() {
    }
     
    public Store( final String name ) {
        this.name = name;
    }
 
    public ObjectId getId() {
        return id;
    }
 
    protected void setId( final ObjectId id ) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName( final String name ) {
        this.name = name;
    }
 
    public List< Stock > getStock() {
        return stock;
    }
 
    public void setStock( final List< Stock > stock ) {
        this.stock = stock;
    }
 
    public Location getLocation() {
        return location;
    }
 
    public void setLocation( final Location location ) {
        this.location = location;
    }
 
    public long getVersion() {
        return version;
    }
 
    public void setVersion(long version) {
        this.version = version;
    }
}

Единственное, что вводит этот класс, — это местоположение и геопространственный индекс, помеченный @Indexed ( IndexDirection. GEO2D ). В MongoDB есть несколько способов представления координат местоположения:

  • как массив координат: [55.5, 42.3]
  • как внедренный объект с двумя свойствами: {lon: 55.5, lat: 42.3}

Класс Location встроенный в класс Store использует второй параметр и является простым как обычный Java-компонент, аналогичный классу Publisher .

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
package com.javacodegeeks.mongodb;
 
import org.mongodb.morphia.annotations.Property;
 
public class Location {
    @Property private double lon;
    @Property private double lat;
 
    public Location() {
    }
     
    public Location( final double lon, final double lat ) {
        this.lon = lon;
        this.lat = lat;
    }
     
    public double getLon() {
        return lon;
    }
     
    public void setLon( final double lon ) {
        this.lon = lon;
    }
 
    public double getLat() {
        return lat;
    }
 
    public void setLat( final double lat ) {
        this.lat = lat;
    }
}

Класс Stock содержит книгу и ее доступное количество в каждом магазине, очень похожее на классы Publisher и Location .

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
package com.javacodegeeks.mongodb;
 
import javax.validation.Valid;
import javax.validation.constraints.Min;
 
import org.mongodb.morphia.annotations.Property;
import org.mongodb.morphia.annotations.Reference;
 
public class Stock {       
    @Reference @Valid private Book book;
    @Property @Min( 0 ) private int quantity;
     
    public Stock() {
    }
     
    public Stock( final Book book, final int quantity ) {
        this.book = book;
        this.quantity = quantity;
    }
     
    public Book getBook() {
        return book;
    }
     
    public void setBook( final Book book ) {
        this.book = book;
    }
 
    public int getQuantity() {
        return quantity;
    }
 
    public void setQuantity( final int quantity ) {
        this.quantity = quantity;
    }
}

Свойство quantity имеет определенное ограничение проверки @Min (0), которое утверждает, что значение этого свойства никогда не должно быть отрицательным.

5. Подключение

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

В Morphia первым шагом является создание экземпляра класса Morhia и его инициализация с желаемыми расширениями (в нашем случае это расширение проверки).

1
2
final Morphia morphia = new Morphia();
new ValidationExtension( morphia );

После выполнения этого шага экземпляр класса Datastore (фактически, база данных MongoDB ) может быть создан (если база данных не существует) или получен (если существует) с использованием экземпляра класса Morhia . Подключение MongoDB требуется на этом этапе и может быть предоставлено с использованием MongoClient класса MongoClient . Кроме того, сопоставленные объекты ( Автор, Книга, Магазин ) могут быть указаны одновременно, что приводит к созданию соответствующих коллекций документов.

1
2
3
4
final MongoClient client = new MongoClient( "localhost", 27017 );
final Datastore dataStore = morphia
    .map( Store.class, Book.class, Author.class )
    .createDatastore( client, "bookstore" );

В случае, если хранилище данных (или база данных) создается с нуля, удобно попросить Morphia также создать все индексы и ограниченные коллекции.

1
2
dataStore.ensureIndexes();
dataStore.ensureCaps();

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

6. Создание документов

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

1
2
3
4
5
6
7
8
9
@Test
public void testCreateNewAuthor() {
    assertThat( dataStore.getCollection( Author.class ).count() ).isEqualTo( 0 );
         
    final Author author = new Author( "Kristina", "Chodorow" );
    dataStore.save( author );
         
    assertThat( dataStore.getCollection( Author.class ).count() ).isEqualTo( 1 );
}

Для любопытных читателей давайте запустим оболочку MongoDB и дважды проверим, что коллекция авторов действительно содержит недавно созданного автора: bin/mongo bookstore

Рисунок 1. Коллекция авторов содержит недавно созданного автора.

Рисунок 1. Коллекция авторов содержит недавно созданного автора.

Ну, это было легко. Но что произойдет, если автор, которого мы собираемся сохранить в MongoDB, нарушит ограничения валидации? В этом случае вызов метода save() завершится с VerboseJSR303ConstraintViolationException как VerboseJSR303ConstraintViolationException в нашем следующем тестовом примере.

1
2
3
4
5
@Test( expected = VerboseJSR303ConstraintViolationException.class )
public void testCreateNewAuthorWithEmptyLastName() {
    final Author author = new Author( "Kristina", "" );
    dataStore.save( author );
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Test
public void testCreateNewBook() {
    assertThat( dataStore.getCollection( Author.class ).count() ).isEqualTo( 0 );
    assertThat( dataStore.getCollection( Book.class ).count() ).isEqualTo( 0 );
         
    final Publisher publisher = new Publisher( "O'Reilly" );
    final Author author = new Author( "Kristina", "Chodorow" );
         
    final Book book = new Book( "MongoDB: The Definitive Guide" );
    book.getAuthors().add( author );
    book.setPublisher( publisher );
    book.setPublishedDate( new LocalDate( 2013, 05, 23 ).toDate() );       
         
    dataStore.save( author );
    dataStore.save( book );
         
    assertThat( dataStore.getCollection( Author.class ).count() ).isEqualTo( 1 );
    assertThat( dataStore.getCollection( Book.class ).count() ).isEqualTo( 1 );       
}

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

Рисунок 2. Коллекция книг содержит только что созданную книгу.

Рисунок 2. Коллекция книг содержит только что созданную книгу.

Чтобы увидеть некоторые нарушения ограничений проверки, давайте попробуем сохранить новую книгу без набора свойств publisher . Это должно привести к исключению VerboseJSR303ConstraintViolationException .

01
02
03
04
05
06
07
08
09
10
11
@Test( expected = VerboseJSR303ConstraintViolationException.class )
public void testCreateNewBookWithEmptyPublisher() {
    final Author author = new Author( "Kristina", "Chodorow" );
         
    final Book book = new Book( "MongoDB: The Definitive Guide" );
    book.getAuthors().add( author );
    book.setPublishedDate( new LocalDate( 2013, 05, 23 ).toDate() );       
         
    dataStore.save( author );
    dataStore.save( book );        
}

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

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
@Test
public void testCreateNewStore() {
    assertThat( dataStore.getCollection( Author.class ).count() ).isEqualTo( 0 );
    assertThat( dataStore.getCollection( Book.class ).count() ).isEqualTo( 0 );       
    assertThat( dataStore.getCollection( Store.class ).count() ).isEqualTo( 0 );
 
    final Publisher publisher = new Publisher( "O'Reilly" );
    final Author author = new Author( "Kristina", "Chodorow" );
        
    final Book book = new Book( "MongoDB: The Definitive Guide" );       
    book.setPublisher( publisher );
    book.setPublishedDate( new LocalDate( 2013, 05, 23 ).toDate() );
    book.getAuthors().add( author );
    book.getCategories().addAll( Arrays.asList( "Databases", "Programming", "NoSQL" ) );
        
    final Store store = new Store( "Waterstones Piccadilly" );
    store.setLocation( new Location( -0.135484, 51.50957 ) );
    store.getStock().add( new Stock( book, 10 ) );
 
    dataStore.save( author );
    dataStore.save( book );     
    dataStore.save( store );
        
    assertThat( dataStore.getCollection( Author.class ).count() ).isEqualTo( 1 );
    assertThat( dataStore.getCollection( Book.class ).count() ).isEqualTo( 1 );       
    assertThat( dataStore.getCollection( Store.class ).count() ).isEqualTo( 1 );
}
Фото 3. Коллекция магазинов содержит только что созданный магазин.

Фото 3. Коллекция магазинов содержит только что созданный магазин.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Test( expected = VerboseJSR303ConstraintViolationException.class )
public void testCreateNewStoreWithNegativeBookQuantity() {
    final Author author = new Author( "Kristina", "Chodorow" );
        
    final Book book = new Book( "MongoDB: The Definitive Guide" );
    book.getAuthors().add( author );
    book.setPublisher( new Publisher( "O'Reilly" ) );
    book.setPublishedDate( new LocalDate( 2013, 05, 23 ).toDate() );       
        
    final Store store = new Store( "Waterstones Piccadilly" );
    store.getStock().add( new Stock( book, -1 ) );
 
    dataStore.save( author );
    dataStore.save( book );     
    dataStore.save( store );
}

7. Запрос документов

Обладая знаниями о том, как создавать новые документы и сохранять их в MongoDB с помощью Morphia, мы теперь готовы взглянуть на запросы к существующим документам. Как мы увидим очень скоро, Morphia предоставляет гибкий и простой в использовании, строго типизированный (где это имеет смысл) API запросов. Чтобы сделать следующие тестовые примеры немного проще, пара авторов , книги :

  • MongoDB: Полное руководство Кристины Шодоров, опубликованное O’Reilly (23 мая 2013 г.) в категориях «Базы данных», Программирование, NoSQL
  • MongoDB Прикладные шаблоны проектирования Рика Коупленда, опубликованные O’Reilly (19 марта 2013 г.) в категориях Базы данных, Программирование, NoSQL, Шаблоны
  • MongoDB в действии Kyle Banker, опубликовано Мэннингом (16 декабря 2011 г.) в категориях Базы данных, Программирование, NoSQL
  • NoSQL Distilled: краткое руководство по формирующемуся миру стойкости полиглота Прамода Дж. Садаладжа и Мартина Фаулера, опубликованное Addison Wesley (18 августа 2012 г.) в категориях Базы данных, NoSQL
  • и магазины :

  • Waterstones Piccadilly расположен по адресу (51.50957, -0.135484) и снабжен:
    • MongoDB: полное руководство в количестве 10
    • MongoDB Applied Design в количестве 45
    • MongoDB в действии в количестве 2
    • NoSQL перегоняется в количестве 0
  • Компания Barnes & Noble расположена по адресу (40.786277, -73.978693) и снабжена:
    • MongoDB: полное руководство в количестве 7
    • MongoDB Applied Design в количестве 12 штук
    • MongoDB в действии в количестве 15
    • NoSQL перегоняется в количестве 2

предварительно заполняются перед каждым тестом.

Тестовый пример, который мы собираемся начать с простых запросов ко всем книгам, в названии которых есть слово «mongodb», с использованием сравнения без учета регистра.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void testFindBooksByName() {
    final List< Book > books = dataStore.createQuery( Book.class )
        .field( "title" ).containsIgnoreCase( "mongodb" )
        .order( "title" )
        .asList();
         
    assertThat( books ).hasSize( 3 );
    assertThat( books ).extracting( "title" )
        .containsExactly(
            "MongoDB Applied Design Patterns",
            "MongoDB in Action",
            "MongoDB: The Definitive Guide"
        );
}

В этом тестовом примере используется один из преимуществ API запросов Morphia. Но есть и другой, использующий вызов метода менее подробного filter который демонстрируется в следующем тестовом примере, просматривая книгу ее автора.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Test
public void testFindBooksByAuthor() {
    final Author author = dataStore.createQuery( Author.class )
        .filter( "lastName =", "Banker" )
        .get();
         
    final List< Book > books = dataStore.createQuery( Book.class )
        .field( "authors" ).hasThisElement( author )
        .order( "title" )
        .asList();
         
    assertThat( books ).hasSize( 1 );
    assertThat( books ).extracting( "title" ).containsExactly( "MongoDB in Action" );       
}

Следующий тестовый пример использует немного более сложные составные критерии: он запрашивает все книги в категориях NoSQL, Базы данных и опубликован после 1 января 2013 года . Результат также сортируется по названию книги в порядке возрастания ( -title ).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Test
public void testFindBooksByCategoryAndPublishedDate() {
    final Query< Book > query = dataStore.createQuery( Book.class ).order( "-title" );       
         
    query.and(
        query.criteria( "categories" )
            .hasAllOf( Arrays.asList( "NoSQL", "Databases" ) ),
        query.criteria( "published" )
            .greaterThan( new LocalDate( 2013, 01, 01 ).toDate() )
    );
             
    final List< Book > books =  query.asList();       
    assertThat( books ).hasSize( 2 );
    assertThat( books ).extracting( "title" )
        .containsExactly(
            "MongoDB: The Definitive Guide",
            "MongoDB Applied Design Patterns"
        );
}

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

01
02
03
04
05
06
07
08
09
10
11
@Test
public void testFindStoreClosestToLondon() {
    final List< Store > stores = dataStore                
        .createQuery( Store.class )
        .field( "location" ).near( 51.508035, -0.128016, 1.0 )
        .asList();
 
    assertThat( stores ).hasSize( 1 );
    assertThat( stores ).extracting( "name" )
        .containsExactly( "Waterstones Piccadilly" );
}

И наконец, было бы неплохо знать, содержит ли книжный магазин, который вы собираетесь посетить, достаточно копий, чтобы вы могли его купить (при условии, что вы хотели бы купить хотя бы 10 из них).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Test
public void testFindStoreWithEnoughBookQuantity() {
    final Book book = dataStore.createQuery( Book.class )
        .field( "title" ).equal( "MongoDB in Action" )
        .get();
             
    final List< Store > stores = dataStore
        .createQuery( Store.class )
        .field( "stock" ).hasThisElement(
            dataStore.createQuery( Stock.class )
                .field( "quantity" ).greaterThan( 10 )
                .field( "book" ).equal( book )
                .getQueryObject() )
        .retrievedFields( true, "name" )
        .asList();
             
    assertThat( stores ).hasSize( 1 );       
    assertThat( stores ).extracting( "name" ).containsExactly( "Barnes & Noble" );       
}

Этот тестовый пример демонстрирует очень важную технику для запроса встроенных (или внутренних) документов с $elemMatch оператора $elemMatch . Поскольку у каждого магазина есть собственный склад (представленный в виде списка объектов Stock ), мы хотели бы найти магазин, у которого есть как минимум 10 копий MongoDB в книге действий. Кроме того, мы хотели бы получить из MongoDB только название магазина, отфильтровывая все остальные свойства, применяя фильтр retrievedFields . Кроме того, каждый элемент акции содержит ссылку на книгу, и поэтому рассматриваемая книга должна быть извлечена заранее и передана в критерии запроса.

8. Обновление документов

Обновления или модификации документов являются наиболее интересными и мощными функциями MongoDB . Он в значительной степени использует возможности запросов, которые мы видели в разделе « Запрос документов », и богатую семантику обновлений, которую мы собираемся изучить.

Неудивительно, что Morphia предоставляет несколько способов обновления документов на месте, используя семейства методов save()/merge() , update()/updateFirst() и findAndModify() . Мы собираемся начать с тестового примера, используя метод save() как мы уже видели в действии.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void testSaveStoreWithNewLocation() {
    final Store store = dataStore
        .createQuery( Store.class )
        .field( "name" ).equal( "Waterstones Piccadilly" )
        .get();
         
    assertThat( store.getVersion() ).isEqualTo( 1 );
         
    store.setLocation( new Location( 50.50957,-0.135484 ) );
    final Key< Store > key = dataStore.save( store );              
         
    final Store updated = dataStore.getByKey( Store.class, key );
    assertThat( updated.getVersion() ).isEqualTo( 2 );       
}

Это самый простой способ выполнить изменения, если вы уже работаете с некоторыми экземплярами документов, полученными из MongoDB . Вы просто вызываете обычные установщики Java для обновления некоторых свойств и передаете обновленный объект методу save() . Обратите внимание, что каждый вызов метода save () увеличивает версию документа: если версия сохраняемого объекта не совпадает с версией в базе данных, ConcurrentModificationException (как смоделируется в приведенном ниже тестовом примере).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Test( expected = ConcurrentModificationException.class )
public void testSaveStoreWithConcurrentUpdates() {
    final Query< Store > query = dataStore
        .createQuery( Store.class )
        .filter( "name =", "Waterstones Piccadilly" );
         
    final Store store1 = query.get();       
    final Store store2 = query.cloneQuery().get();
         
    store1.setName( "New Store 1" );
    dataStore.save( store1 );   
    assertThat( store1.getName() ).isEqualTo( "New Store 1" );  
         
    store2.setName( "New Store 2" );
    dataStore.save( store2 );                             
}

Другой способ выполнения единичных или массовых изменений документа — использование вызова метода update() . По умолчанию update() изменяет все документы, соответствующие критериям запроса. Если вы хотите ограничить область действия только одним документом, используйте updateFirst() .

01
02
03
04
05
06
07
08
09
10
11
12
13
@Test
public void testUpdateStoreLocation() {
    final UpdateResults< Store > results = dataStore.update(
        dataStore
            .createQuery( Store.class )
            .field( "name" ).equal( "Waterstones Piccadilly" ),            
        dataStore
            .createUpdateOperations( Store.class )
            .set( "location", new Location( 50.50957,-0.135484 ) )
    );
         
    assertThat( results.getUpdatedCount() ).isEqualTo( 1 );       
}

Возвращаемое значение метода update() — это не документ (или документы), а более общий статус операции: сколько документов было обновлено или вставлено и т. Д. Это очень полезно и быстрее по сравнению с другими параметрами, если вам не требуется обновленный документ (ы) должен быть возвращен вам, но, чтобы убедиться, что по крайней мере что-то было обновлено.

findAndModify — самая мощная и многофункциональная операция. Это приводит к тем же результатам, что и метод update() но также может возвращать обновленный документ (после применения изменений) или старый (до внесения изменений). Кроме того, он может создать новый документ, если ни один не соответствует запросу (выполнение так называемого upsert ).

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
@Test
public void testFindStoreWithEnoughBookQuantity() {
    final Book book = dataStore.createQuery( Book.class )
        .field( "title" ).equal( "MongoDB in Action" )
        .get();
         
    final Store store = dataStore.findAndModify(
        dataStore
            .createQuery( Store.class )
            .field( "stock" ).hasThisElement(
                dataStore.createQuery( Stock.class )
                    .field( "quantity" ).greaterThan( 10 )
                    .field( "book" ).equal( book )
                    .getQueryObject() ),            
        dataStore
            .createUpdateOperations( Store.class )
            .disableValidation()
            .inc( "stock.$.quantity", -10 ),
        false
    );
         
    assertThat( store ).isNotNull();       
    assertThat( store.getStock() )
        .usingElementComparator( comparator )
        .contains( new Stock( book, 5 ) );
}

Приведенный выше тестовый пример находит хранилище, у которого есть как минимум 10 копий книги MongoDB в действии, и уменьшает ее запас на эту сумму (оставляя только 5 копий из первоначальных 15 ). Конструкция stock. $. Amount предназначена для обновления точного элемента stock, соответствующего критериям запроса. Модифицированный магазин был возвращен звонком, подтверждающим, что на складе осталось только 5 MongoDB в книгах действий.

9. Удаление документов

Как и при обновлении документов , удаление документов из коллекции может быть выполнено несколькими способами, в зависимости от варианта использования. Например, если у вас уже есть экземпляр документа, извлеченный из MongoDB , он может быть передан непосредственно в метод delete() .

01
02
03
04
05
06
07
08
09
10
@Test
public void testDeleteStore() {
    final Store store = dataStore
        .createQuery( Store.class )
        .field( "name" ).equal( "Waterstones Piccadilly" )
        .get();
         
    final WriteResult result = dataStore.delete( store );
    assertThat( result.getN() ).isEqualTo( 1 );               
}

Метод возвращает статус операции с несколькими затронутыми документами. Поскольку мы удаляем один экземпляр хранилища , ожидаемое количество затронутых документов равно 1 .

В случае массового удаления метод delete() позволяет предоставить запрос, а также возвращает статус с рядом затронутых документов. Следующий тестовый пример удаляет все магазины (которые мы создали только 2 ).

1
2
3
4
5
@Test
public void testDeleteAllStores() {
    final WriteResult result = dataStore.delete( dataStore.createQuery( Store.class ) );
    assertThat( result.getN() ).isEqualTo( 2 );               
}

Следовательно, существует метод findAndDelete() который принимает запрос и возвращает один (или первый в случае нескольких совпадений) удаленный документ, который удовлетворяет критериям. Если ни один документ не соответствует критериям, findAndDelete() возвращает findAndDelete() .

01
02
03
04
05
06
07
08
09
10
11
@Test
public void testFindAndDeleteBook() {
    final Book book = dataStore.findAndDelete(
        dataStore
            .createQuery( Book.class )
            .field( "title" ).equal( "MongoDB in Action" )
        );
         
    assertThat( book ).isNotNull();               
    assertThat( dataStore.getCollection( Book.class ).count() ).isEqualTo( 3 );
}

10. Агрегации

Агрегации являются наиболее сложной частью операций MongoDB из-за гибкости, которую они предоставляют для манипулирования документами. Морфия изо всех сил пытается упростить использование агрегатов из кода Java, но вы все равно можете столкнуться с некоторыми трудностями при работе с ними.

Мы собираемся начать с простого примера группировки книг по издателю. Результатом операции является имя издателя и количество книг.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testGroupBooksByPublisher() {
    final DBObject result = dataStore
        .getCollection( Book.class )
        .group(
            new BasicDBObject( "publisher.name", "1" ),
            new BasicDBObject(),
            new BasicDBObject( "total", 0 ),
            "function ( curr, result ) { result.total += 1 }"
        );       
    assertThat( result ).isInstanceOf( BasicDBList.class );
                          
    final BasicDBList groups = ( BasicDBList )result;
    assertThat( groups ).hasSize( 3 );       
    assertThat( groups ).containsExactly(
        new BasicDBObject( "publisher.name", "O'Reilly" ).append( "total", 2.0 ),
        new BasicDBObject( "publisher.name", "Manning" ).append( "total", 1.0 ),
        new BasicDBObject( "publisher.name", "Addison Wesley" ).append( "total", 1.0 )
    );
}

Как вы можете видеть, даже будучи вызовом метода Java, аргументы group () во многом напоминают параметры групповой команды, которые мы видели в части 2. Руководство по оболочке MongoDB — Операции и команды .

Чтобы закончить с агрегациями, давайте рассмотрим более интересную задачу: сгруппировать книги по категориям. На данный момент MongoDB не поддерживает группирование по свойству массива (какие категории). К счастью, мы можем построить конвейер агрегации (который мы также затронули в части 2. Руководство по оболочке MongoDB — Операции и команды ) для достижения желаемого результата.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testGroupBooksByCategories() {
    final DBCollection collection = dataStore.getCollection( Book.class );
         
    final AggregationOutput output = collection
        .aggregate(
            Arrays.< DBObject >asList(
                new BasicDBObject( "$project", new BasicDBObject( "title", 1 )
                   .append( "categories", 1 ) ),
                new BasicDBObject( "$unwind", "$categories"),
                new BasicDBObject( "$group", new BasicDBObject( "_id", "$categories" )
                   .append( "count", new BasicDBObject( "$sum", 1 ) ) )
            )              
        );
         
    assertThat( output.results() ).hasSize( 4 );
    assertThat( output.results() ).containsExactly(             
        new BasicDBObject( "_id", "Patterns" ).append( "count", 1 ),
        new BasicDBObject( "_id", "Programming" ).append( "count", 3 ),
        new BasicDBObject( "_id", "NoSQL" ).append( "count", 4 ),           
        new BasicDBObject( "_id", "Databases" ).append( "count", 4 )              
    );
}

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

11. Что дальше

Эта часть дала нам первый взгляд на MongoDB с точки зрения разработчика приложений. В следующей части урока мы рассмотрим возможности шардинга MongoDB .