MongoDB和Java教程

本文是我们学院课程中名为MongoDB –可扩展NoSQL DB的一部分

在本课程中,您将被介绍到MongoDB。 您将学习如何安装它以及如何通过它的外壳进行操作。 此外,您还将学习如何通过Java以编程方式访问它以及如何将Map Reduce与其一起使用。 最后,将解释更高级的概念,例如分片和复制。 在这里查看

1.简介

在本教程的前面部分中,我们简要浏览了许多MongoDB功能,其体系结构,安装以及大多数受支持的命令。 希望您已经看到MongoDB非常适合您的应用程序需求,并希望使其成为软件堆栈的一部分。

在这一部分中,我们将介绍MongoDB与用Java编写的应用程序的集成。 我们选择Java的原因是Java的普及,但是MongoDB提供了许多其他语言的绑定(或驱动程序),有关完整列表,请参阅官方文档

我们将开发一个简单的书店应用程序,其目标是涵盖您可能会遇到的大多数用例,并强调MongoDB解决它们的方法。 这些解决方案将以典型的JUnit测试用例的形式呈现,它们由AssertJ提供出色的流利断言, 并不时伴随MongoDB Shell命令作为验证步骤。

尽管到目前为止, Spring Data MongoDB是Java社区中最受欢迎的选择,但我们将使用另一个称为Morphia的出色库:轻量级类型安全的库,用于将Java对象映射到MongoDB或从MongoDB映射。 撰写本文时, Morphia库的最新版本为0.106 ,但不幸的是,它尚不支持MongoDB 2.6的所有功能(如文本索引和全文搜索)。

2.扩展

Morphia的另一个优点是它的可扩展性。 特别是,我们对Morphia团队提供的开箱即用的JSR 303:Bean Validation 1.0扩展非常感兴趣。 这是非常有用的补充,有助于确保流经系统的数据对象有效且有意义。 稍后我们将看到几个示例。

3.依存关系

我们的项目将使用Apache Maven进行构建和依赖关系管理,因为它在Java社区中是相当受欢迎的选择。 幸运的是,可以通过公共Apache Maven存储库获得Morphia版本,我们将利用这三个模块:

<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验证1.0的集成。 最后,日志记录模块与出色的SLF4J框架集成在一起。

4.数据模型

当我们构建一个简单的书店应用程序时,其数据域可能由这三个类表示:

  • 作者 :这本书的作者
  • :书本身
  • 商店 :出售书籍的商店

为了投影我们刚才对MongoDB术语所说的话,我们将创建一个包含三个文档集合的数据库书店

  • 作者 :所有知名书籍作者
  • 书籍 :所有可用书籍
  • 商店 :所有出售书籍的现有商店

Java类( 作者,书籍,商店 )和MongoDB集合( 作者,书籍,商店 )之间的透明映射是Morphia的职责之一。 首先让我们看一下Author类,因为它是最简单的类。

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;
    }
}

对于使用JPA和/或Hibernate的 Java开发人员,这样的POJO(普通的旧Java对象)非常熟悉。 该@Entity注释的Java类映射到MongoDB的集合,在这种情况下, 作者 - > 作者 。 关于noClassnameStored属性的简要说明:默认情况下, Morphia将完整的Java类名称存储在每个MongoDB文档中。 如果同一集合可能包含不同类型的文档(表示不同Java类的实例),则实际上非常有用。 在我们的应用程序中,我们将不使用这种功能,因此指示Morphia不要存储此信息(这会使文档看起来更简洁 )。

接下来,我们将MongoDB文档字段声明为Java类的属性。 对于Author ,它包括:

  • id (标有@Id)
  • 版本 (标有@Version),在并发更新的情况下,我们将使用此属性进行乐观锁定
  • firstName (标记为@Property),此外,它还定义了@NotEmpty验证约束,该约束要求设置此属性
  • lastName (标记为@Property),此外,它还定义了@NotEmpty验证约束,该约束要求设置此属性

最后, Author类为lastNamefirstName属性定义一个复合索引(索引概念将在第7部分中深入讨论。《 MongoDB安全性,性能分析,索引,游标和批量操作指南》 )。

让我们继续进行一些更复杂的示例,其中涉及另外两个类BookStore 。 这是一Book课。

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;
   }
}

我们已经看到了大多数注释,但是有几个新注释:

  • publishedDate (在文档中标记为@Property的名称为“ published”)
  • 发布者 (标记为@Embedded,整个对象将存储在每个文档中),此外,它还定义了@Validand @NotNull验证约束,该约束要求设置此属性并使其有效(符合自己的验证约束)
  • authors (标记为@Reference,对作者的引用将存储在每个文档中),此外,它还定义了@Valid验证约束,该约束要求集合中的每个作者都有效(符合自己的验证约束)

titlecategories属性具有使用@Indexed批注定义的自己的索引。 Publisher类未使用@Entity注释,因此未映射到任何简单Java bean的MongoDB集合。 话虽这么说, Publisher类仍然可以声明索引,该索引将成为该类嵌入在( book )中的MongoDB集合的一部分。

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类。 此类基本上将我们在AuthorBook类中看到的所有概念粘合在一起。

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}

嵌入在Store类内部的Location类使用第二个选项,它像常规Java bean一样简单,类似于Publisher类。

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类在每个商店中保存该书及其可用数量,与PublisherLocation类非常相似。

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实例并在localhost和默认端口27017上运行

使用Morphia的第一步是创建Morhia类的实例,并使用所需的扩展名对其进行初始化(在我们的示例中,这是一个验证扩展名)。

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

完成此步骤后,可以使用Morhia类的实例创建(如果数据库不存在)或获取(如果存在) Datastore类的实例(实际上是MongoDB数据库)。 MongoDB连接在此阶段是必需的,可以使用MongoClient类实例提供。 另外,可以同时指定映射的实体( Author,Book,Store ),从而创建相应的文档集合。

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

如果数据存储(或数据库)是从头开始创建的,可以方便地要求Morphia创建所有索引和上限集合。

dataStore.ensureIndexes();
dataStore.ensureCaps();

太好了,有了适当的数据存储实例,我们就可以创建新文档,运行查询以及执行复杂的更新和聚合。

6.创建文件

最好从创建几个文档并将它们保存在MongoDB中开始 。 我们的第一个测试用例就是通过创建新作者来做到这一点。

@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 shell并仔细检查authors集合是否确实包含新创建的作者: bin/mongo bookstore

图1. authors集合包含新创建的author。

图1. authors集合包含新创建的author。

好吧,那很容易。 但是,如果我们将要坚持使用MongoDB的作者违反了验证约束,该怎么办? 在这种情况下, save()方法调用将因VerboseJSR303ConstraintViolationException异常而失败,如我们的下一个测试案例所示。

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

无需编写任何代码,只需声明对适当对象状态的某些期望,我们就可以免费执行字面上的复杂验证任务。 在下一个测试用例中,我们将做更多工作以保留新书。

@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 shell,我们确保新创建的书存储在books集合中,并且其authors属性引用authors集合中的文档: bin/mongo bookstore

图片2.图书集合包含新创建的图书。

图片2.图书集合包含新创建的图书。

要查看违反验证约束的条件,让我们尝试在未设置publisher属性的情况下存储新书。 它应导致VerboseJSR303ConstraintViolationException异常。

@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 );         
}

最终测试用例说明了新商店的创建以及在库存数量为负数的书时的工作验证。

@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.商店集合包含新创建的商店。

@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.查询文件

了解了如何创建新文档并使用Morphia将其存储在MongoDB中的知识之后,我们现在就可以开始查询现有文档了。 我们很快就会看到, Morphia提供了流畅且易于使用的强类型(有意义的)查询API。 为了简化以下测试用例,几对作者书籍

  • MongoDB:由Kristina Chodorow撰写的权威指南,由O'Reilly于2013年5月23日出版,类别为Databases”,编程,NoSQL
  • Rick Copeland撰写的MongoDB应用设计模式,由O'Reilly发布(2013年3月19日),类别为数据库,编程,NoSQL,模式
  • 由Kyle Banker编写的《 MongoDB in Action》,由Manning发布(2011年12月16日),类别为数据库,编程,NoSQL
  • NoSQL Distilled:由Polymod J Sadalage和Martin Fowler撰写的多语言持久性新兴世界简要指南,由Addison Wesley(2012年8月18日)出版,类别为数据库,NoSQL
  • 商店

  • 位于(51.50957,-0.135484)的Waterstones Piccadilly处有:
    • MongoDB:权威指南,数量为10
    • MongoDB应用设计的数量为45
    • 运行中的MongoDB数量为2
    • NoSQL蒸馏数量为0
  • Barnes&Noble位于(40.786277,-73.978693),库存:
    • MongoDB:权威指南,数量为7
    • MongoDB应用设计的数量为12
    • MongoDB运行中的数量为15
    • NoSQL蒸馏数量为2

在每次测试运行之前已经预先填充了。

我们将使用不区分大小写的比较,从仅对标题中带有“ mongodb”字样的所有书籍的查询开始测试案例。

@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" 
        );
}

该测试用例使用了Morphia查询API的优势之一。 但是还有另一个使用不太冗长的filter方法调用的方法,下一个测试用例通过查看作者的书来演示。

@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,Databases类别中的所有书籍,并于2013年1月1日之后发布。 结果也按书名的升序( “ -title” )排序。

@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的地理空间查询功能。

@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本)。

@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运算符查询嵌入式(或内部)文档的一种非常重要的技术。 因为每个商店都有自己的库存(表示为Stock对象的列表),所以我们想在“行动”书中找到至少有10个MongoDB副本的商店。 此外,我们希望仅从MongoDB中检索商店名称,通过应用retrievedFields过滤器过滤掉所有其他属性。 此外,每个stock元素都包含对该书的引用,因此,应事先检索有问题的书,并将其传递给查询条件。

8.更新文件

文档更新或修改是MongoDB最有趣,最强大的功能。 它在很大程度上利用了我们在“ 查询文档”部分中看到的查询功能以及我们将要探索的丰富更新语义。

毫不奇怪, Morphia提供了几种使用save()/merge()update()/updateFirst()findAndModify()方法族执行就地文档更新的方法。 我们将从使用用过的save()方法开始测试用例开始。

@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 setter来更新一些属性,然后将更新的对象传递给save()方法。 请注意,每次调用save()都会增加文档的版本:如果要保存的对象的版本与数据库中的版本不匹配,则会引发ConcurrentModificationException (如下面的测试用例所模拟)。

@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()方法。

@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 )。

@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的商店,并将库存减少了该数量(在最初的15本书中只有5本书可用)。 stock。$。quantity构造的目标是对与查询条件匹配的确切库存元素进行更新。 调用返回了修改后的存储,确认仅剩5本书在操作中。

9.删除文件

更新文档类似,根据使用情况,可以通过多种方式从集合中删除文档。 例如,如果您已经有一个从MongoDB检索到的文档实例,则可以将其直接传递给delete()方法。

@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个 )。

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

因此,存在一个findAndDelete()方法,该方法接受查询并返回满足条件的单个(或多个匹配项,则是第一个)已删除文档。 如果没有文档符合条件,则findAndDelete()返回null

@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操作中最复杂的部分,因为它们提供了操作文档的灵活性。 Morphia努力简化Java代码中聚合的用法,但是您仍然可能会发现使用它们有点困难。

我们将从简单的按出版商对书籍进行分组的示例开始。 操作的结果是出版商的名称和书籍数量。

@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部分中看到的group命令的参数。MongoDB Shell指南–操作和命令

为了完成汇总,让我们处理一些更有趣的任务:按类别对书籍进行分组。 目前, MongoDB不支持按数组属性分组(属于哪些类别)。 幸运的是,我们可以构建一个聚合管道 (在第2部分中已经涉及到该管道 。MongoDB Shell指南–操作和命令 )可以实现所需的结果。

@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分片功能。

翻译自: https://www.javacodegeeks.com/2015/09/mongodb-and-java-tutorial.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值