Spring BootとSpring Data MongoDBを使って検索アプリケーションを開発する

# 概要

Spring BootとSpring Data MongoDBを使用して、簡単な検索ができるWebアプリケーションを開発します。

**環境**

* Windows7 (64bit)
* Java 1.8.0_60
* Spring-Boot 1.2.5
  * Spring-Data-MongoDB 1.6.3
* MongoDB 2.4.14


**参考**

下記のサイトを参考にさせていただきました。

* [Spring Data MongoDB - Reference Documentation] (http://docs.spring.io/spring-data/data-mongo/docs/1.6.3.RELEASE/reference/html/)

**完成図**

![customers.png](https://qiita-image-store.s3.amazonaws.com/0/22772/52ef3440-d0b0-8076-a9db-cf6dec6b8abf.png)

![customer_details.png](https://qiita-image-store.s3.amazonaws.com/0/22772/388159d0-20c5-4d16-2006-9226dab2a0ce.png)

**github**

ソースコードは[rubytomato/sbdm-example] (https://github.com/rubytomato/sbdm-example)にあります。


## 事前準備

Spring Data MongoDBがサポートするMongoDBのバージョンは下表の通りです。
この表に従って今回使用するMongoDBのバージョンは2.4.14にしました。

**Spring Data MongoDB Support**

|Spring-Data-MongoDB Version   |Support MongoDB Version             |reference |
|:---------|:-------------------------------------------------------|:---------|
|1.6.3     |Spring MongoDB support requires MongoDB 1.4 or higher   |[MongoDB support] (http://docs.spring.io/spring-data/data-mongo/docs/1.6.3.RELEASE/reference/html/#mongo.core)|
|1.7.2     |Spring MongoDB support requires MongoDB 2.6 or higher   |[MongoDB support] (http://docs.spring.io/spring-data/data-mongo/docs/1.7.2.RELEASE/reference/html/#mongo.core)|
|1.8.0 RC1 |Spring MongoDB support requires MongoDB 2.6 or higher   |[MongoDB support] (http://docs.spring.io/spring-data/data-mongo/docs/1.8.0.RC1/reference/html/#mongo.core)|


### MongoDBの準備

MongoDBのインストール作業の説明は省略します。


#### データベース、アカウントの作成

データベース名: `mongovwdb`
ユーザー/パスワード: `mongovwuser` / `mongovwpass`

```text
> use mongovwdb
```

ユーザーを追加します。

```text
> db.addUser({user:"mongovwuser", pwd:"mongovwpass", roles: [ "userAdmin" ]})
```

認証できるか確認します。

```text
> db.auth("mongovwuser","mongovwpass")
1
```

#### サンプルデータセット

このアプリケーションで扱うデータは[MySQL Sample Database - MySQLTutorial] (http://www.mysqltutorial.org/mysql-sample-database.aspx)で公開されているサンプルデータセットをお借りしました。

**コレクションの一覧**

|name          |description|
|:-------------|:----------|
|customers     |顧客       |
|order_details |注文明細   |
|orders        |注文       |
|payments      |支払い     |
|product_lines |製品種別   |
|products      |製品       |

**データ例**

***customers***

```json
> db.customers.findOne()
{
        "_id" : ObjectId("55df620d67cdfe3cd8549f4b"),
        "_class" : "com.example.sbdm.domain.Customers",
        "customerNumber" : NumberLong(103),
        "customerName" : "Atelier graphique",
        "contactLastName" : "Schmitt",
        "contactFirstName" : "Carine",
        "phone" : "40.32.2555",
        "addressLine1" : "54, rue Royale",
        "city" : "Nantes",
        "postalCode" : "44000",
        "country" : "France",
        "salesRepEmployeeNumber" : NumberLong(1370),
        "creditLimit" : "21000"
}
```

***order_details***

```json
> db.order_details.findOne()
{
        "_id" : ObjectId("55df620d67cdfe3cd8549fc5"),
        "_class" : "com.example.sbdm.domain.OrderDetails",
        "orderNumber" : NumberLong(10100),
        "productCode" : "S18_1749",
        "quantityOrdered" : NumberLong(30),
        "priceEach" : "136",
        "orderLineNumber" : 3
}
```

***orders***

```json
> db.orders.findOne()
{
        "_id" : ObjectId("55df620d67cdfe3cd854ab79"),
        "_class" : "com.example.sbdm.domain.Orders",
        "orderNumber" : NumberLong(10100),
        "orderDate" : ISODate("2011-01-05T15:00:00Z"),
        "requiredDate" : ISODate("2011-01-12T15:00:00Z"),
        "shippedDate" : ISODate("2011-01-09T15:00:00Z"),
        "status" : "Shipped",
        "customerNumber" : NumberLong(363)
}
```

***payments***

```json
> db.payments.findOne()
{
        "_id" : ObjectId("55df620d67cdfe3cd854acbf"),
        "_class" : "com.example.sbdm.domain.Payments",
        "customerNumber" : NumberLong(103),
        "checkNumber" : "HQ336336",
        "paymentDate" : ISODate("2012-10-18T15:00:00Z"),
        "amount" : "6066.78"
}
```

***product_lines***

```json
> db.product_lines.findOne()
{
        "_id" : ObjectId("55df620d67cdfe3cd854add0"),
        "_class" : "com.example.sbdm.domain.ProductLines",
        "productLine" : "Classic Cars",
        "textDescription" : "Attention car enthusiasts: Make yo ...省略..."
}
```

***products***

```json
> db.products.findOne()
{
        "_id" : ObjectId("55df620d67cdfe3cd854add7"),
        "_class" : "com.example.sbdm.domain.Products",
        "productCode" : "S10_1678",
        "productName" : "1969 Harley Davidson Ultimate Chopper",
        "productLine" : "Motorcycles",
        "productScale" : "1:10",
        "productVendor" : "Min Lin Diecast",
        "productDescription" : "This replica features working kickstand, ...省略...",
        "quantityInStock" : 7933,
        "buyPrice" : "48.81",
        "MSRP" : "95.7"
}
```


## アプリケーションの作成

### プロジェクトの雛形を生成

プロジェクト名: `sbdm-example`

**mavenでアプリケーションの雛形を作成**

```text:generate
> mvn archetype:generate -DgroupId=com.example.sbdm -DartifactId=sbdm-example -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
```

```text:eclipse
> cd sbdm-example
> mvn eclipse:eclipse
```

**eclipseにインポート**

* メニューバーの"File" -> "Import..." -> "Maven" -> "Existing Maven Projects"を選択します。
* プロジェクトのディレクトリを選択し、"Finish"ボタンをクリックします。

**pom.xmlの編集**

```xml:pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.sbdm</groupId>
  <artifactId>sbdm-example</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>sbdm-example</name>
  <url>http://maven.apache.org</url>

  <properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.2.5.RELEASE</version>
  </parent>

  <dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    <dependency>
      <groupId>org.codehaus.jackson</groupId>
      <artifactId>jackson-mapper-asl</artifactId>
      <version>1.9.13</version>
    </dependency>
    <dependency>
      <groupId>commons-collections</groupId>
      <artifactId>commons-collections</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.4</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <version>1.2.5.RELEASE</version>
        </plugin>
      </plugins>
    </pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <verbose>true</verbose>
          <source>${java.version}</source>
          <target>${java.version}</target>
          <encoding>${project.build.sourceEncoding}</encoding>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>versions-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
```

**resourcesフォルダの作成**

設定ファイルやテンプレートファイルなどを配置する`resources`フォルダを`src/main`下に作成します。

作成したらプロジェクトに反映させます。

* "Build Path" -> "Configure Build Path" -> "Java Buld Path" -> "Source"タブを選択する。
* "Add Folder"ボタンをクリック -> 作成した"resources"フォルダにチェックを入れる。

**application.ymlの作成**

src/main/resourcesフォルダ内にapplication.ymlを作成します。

```yaml:application.yml
# EMBEDDED SERVER CONFIGURATION (ServerProperties)
server:
  port: 9000

spring:
# THYMELEAF (ThymeleafAutoConfiguration)
  thymeleaf:
    enabled: true
    cache: false
# INTERNATIONALIZATION (MessageSourceAutoConfiguration)
  messages:
    basename: messages
    cache-seconds: -1
    encoding: UTF-8
# MONGODB (MongoProperties)
  data:
    mongodb:
      host: localhost
      port: 27017
      uri: mongodb://localhost/mongovwdb # connection URL
      database: mongovwdb
#      authentication-database: mongovwdb
#      grid-fs-database:
      username: mongovwuser
      password: mongovwpass
      repositories:
        enabled: true  # if spring data repository support is enabled

# ENDPOINTS (AbstractEndpoint subclasses)
endpoints:
  enabled: true
```

**logback.xmlの作成**

src/main/resourcesフォルダ内にlogback.xmlを作成します。
ログの出力先フォルダを"D:/logs"に指定しました。

```xml:logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <property name="LOG_DIR" value="D:/logs" />

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MMM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{35} - %msg %n</pattern>
    </encoder>
  </appender>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
     <file>${LOG_DIR}/sbdm-example.log</file>
     <encoder>
       <charset>UTF-8</charset>
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] - %msg %n</pattern>
     </encoder>
  </appender>

  <logger name="com.example" level="DEBUG" />

  <logger name="org.hibernate" level="ERROR"/>
  <logger name="org.springframework" level="INFO"/>
  <logger name="org.thymeleaf" level="INFO"/>
  <logger name="org.eclipse.jetty" level="INFO"/>
  <logger name="org.apache.http" level="INFO"/>
  <root>
    <appender-ref ref="STDOUT" />
    <appender-ref ref="FILE" />
  </root>
</configuration>
```

**ビルド**

この時点で動作検証を兼ねてビルドします。

```text:package
> mvn package
```

ビルドが成功したら生成したjarファイルを実行します。
コマンドプロンプトに"Hello World!"と表示されれば成功です。

```text:jar
> cd target
> java -jar sbdm-example-1.0-SNAPSHOT.jar
Hello World!
```


### アプリケーションの開発

すべてのソースコードを掲載すると記事サイズが大きくなってしまうのでポイントとなる部分だけ掲載していきます。

#### App

エンドポイントとなるクラスを作成します。
すでにサンプルのApp.javaがありますので、このファイルを下記のように変更します。

```java:App.java
package com.example.sbdm;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import com.example.sbdm.viewhelper.MyDialect;

@SpringBootApplication
public class App {

  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }

  //THYMELEAF Utility Object
  @Bean
  MyDialect myDialect(){
    return new MyDialect();
  }

}
```

#### MongodbConfig

MongoDBの設定を行うクラスを`AbstractMongoConfiguration`を継承して作成します。

```java:MongoDB.java
package com.example.sbdm;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.authentication.UserCredentials;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.data.mongodb.core.WriteResultChecking;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.data.repository.query.QueryLookupStrategy;

import com.mongodb.Mongo;
import com.mongodb.MongoClient;
import com.mongodb.WriteConcern;

@Configuration
@EnableMongoRepositories(
  basePackages = "com.example.sbdm.repository",
  queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND
)
public class MongodbConfig extends AbstractMongoConfiguration {

  @Override
  @Bean
  public Mongo mongo() throws Exception {
    Mongo mongo = new MongoClient("localhost", 27017);
    mongo.setWriteConcern(WriteConcern.SAFE);
    return mongo;
  }

  @Override
  @Bean
  public MongoDbFactory mongoDbFactory() throws Exception {
    return new SimpleMongoDbFactory(mongo(), getDatabaseName(), getUserCredentials());
    //return new SimpleMongoDbFactory(mongo(), getDatabaseName());
  }

  @Override
  @Bean
  public MongoTemplate mongoTemplate() throws Exception {
    MongoTemplate template = new MongoTemplate(mongoDbFactory());
    template.setWriteResultChecking(WriteResultChecking.EXCEPTION);
    return template;
  }

  @Value("${spring.data.mongodb.database}")
  private String databasename;

  @Override
  protected String getDatabaseName() {
    return databasename;
  }

  @Override
  protected String getMappingBasePackage() {
    return "com.example.sbdm.domain";
  }

  @Value("${spring.data.mongodb.username}")
  private String username;
  @Value("${spring.data.mongodb.password}")
  private String password;

  @Override
  protected UserCredentials getUserCredentials() {
    UserCredentials userCredentials = new UserCredentials(username, password);
    return userCredentials;
  }

}
```

#### Domain

パッケージ: `com.example.sbdm.domain`

MongoDBのコレクションに対応するdomainクラスを作成します。

**Customers**

```java:Customers.java
package com.example.sbdm.domain;

import java.math.BigDecimal;

import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.index.IndexDirection;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

@Document(collection = CollectionNames.Customers)
@CompoundIndexes({
  @CompoundIndex(
    name = "idx1_customers",
    def = "{'contactLastName' : 1, 'contactFirstName' : 1}"
  )
})
public class Customers {

  public Customers() {
  }

  @PersistenceConstructor
  public Customers(Long customerNumber, String customerName) {
    this.customerNumber = customerNumber;
    this.customerName = customerName;
  }

  @Id
  private String id;

  @Indexed(name = "pk_customers", direction = IndexDirection.DESCENDING, unique = true)
  @Field("customerNumber")
  private Long customerNumber;

  private String customerName;

  @Field("contactLastName")
  private String contactLastName;

  @Field("contactFirstName")
  private String contactFirstName;

  private String phone;

  private String addressLine1;

  private String addressLine2;

  private String city;

  private String state;

  private String postalCode;

  private String country;

  private Long salesRepEmployeeNumber;

  private BigDecimal creditLimit;

  //...getter/setterは省略します...
}
```

**ポイント**

アノテーションを使用してMongoDBのコレクション/ドキュメントとの関係を定義します。

* `@Id`アノテーションでドキュメントの_idフィールドをマッピングします。
* `@Document`アノテーションでコレクション名を定義します。
* `@Indexed`アノテーションや`@CompoundIndex`アノテーションでインデックスを定義します。
* `@Field`アノテーションでドキュメントのフィールドをマッピングします。


#### Repository

パッケージ: `com.example.sbdm.repository`

`MongoRepository`インターフェースを継承した各コレクション用のrepositoryインターフェースを作成します。

**CustomersRepository**

```java:CustomersRepository.java
package com.example.sbdm.repository;

import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;

import com.example.sbdm.domain.Customers;

public interface CustomersRepository extends MongoRepository<Customers, String> {

  public static final String FIND =
   "{$or:[" +
   "{'customerName': {$regex: '?0', $options: 'i'}}," +
   "{'contactFirstName': {$regex: '?0', $options: 'i'}}," +
   "{'contactLastName': {$regex: '?0', $options: 'i'}}" +
   "]}";

  Customers findByCustomerNumber(Long customerNumber);

  Iterable<Customers> findByCustomerNameLike(String customerName, Sort sort);

  Long countByCustomerNameLike(String keyword);

  @Query(value = FIND, count = true)
  Long searchCount(String keyword);

  @Query(value = FIND)
  PageImpl<Customers> search(String keyword, Pageable page);
}
```

**ポイント**

* このrepositoryインターフェースには`@Repository`アノテーションを付けません。付けるとアプリケーション起動時にエラーが発生します。
* `MongoRepository`インターフェースを継承することで基本的なメソッド(findやcount,save,delete)が使用できるようになります。
* 任意の条件で検索をおこないたい場合は命名ルールに基づいて独自メソッドを定義します。
* 任意のクエリーを発行したい場合は`@Query`アノテーションを使用して実行するクエリーを指定することができます。

**独自メソッドの定義について**

命名ルールに従ったメソッド名にすることで任意のクエリーを発行することができます。
ルールは[Query methods] (http://docs.spring.io/spring-data/data-mongo/docs/1.6.3.RELEASE/reference/html/#repositories.query-methods)で詳細に解説されています。

例えば下記のメソッド定義は、`customers`コレクションの`cusotomerNumber`フィールドに対してパラメータ値と同じ値を持つドキュメントを検索します。（なおこのフィールドは一意なので、1件しか検索されないことが保証されています。)

```java:customize
Customers findByCustomerNumber(Long customerNumber);
```
メソッド名にLikeを加えるとlike検索を行います。

```java:customize
Iterable<Customers> findByCustomerNameLike(String customerName, Sort sort);
```

件数のカウントはcountByを加えます。

```java:customize
Long countByCustomerNameLike(String keyword);
```

複数のフィールドを検索条件にしたい場合はAndやOrを加えます。

```java:customize
OrderDetails findByOrderNumberAndProductCode(Long orderNumber, String prodctCode);
```

`@Query`アノテーションを使えば、実行したいクエリーを直接指定することができます。
ちなみに、この例ではPageableインタフェースを引数に加えていますが、この場合の戻り値の型は`PageImpl`クラスになります。（加えない場合は`ArrayList`クラスになります。)

```java:customize
@Query(value = FIND)
PageImpl<Customers> search(String keyword, Pageable page);
```

`count = true`を加えるとクエリーの結果をカウントします。

```java:customize
@Query(value = FIND, count = true)
Long searchCount(String keyword);
```

#### Service

パッケージ: `com.example.sbdm.service`

ドキュメント操作を行う基本的なメソッドを定義したインタフェースを作成します。

**IService**

```java:IService.java
package com.example.sbdm.service;


public interface IService<T> extends Pagination {

  /**
   * コレクションのドキュメントの件数
   */
  public long count();

  /**
   * ドキュメントIDで検索
   */
  public T findById(String id);

  /**
   * 条件なしでドキュメントを検索
   */
  public Iterable<T> findAll(int page, int size, String sortColumn);

  /**
   * ドキュメントのプライマリーキーで検索
   */
  public T findByPk(Object...keys);

  /**
   * 名称のlike検索
   */
  public Iterable<T> findByNameLike(String name, String sortColumn);

  /**
   * 検索ワードに一致するドキュメントの件数
   */
  public long searchCount(String keyword);

  /**
   * 検索ワードに一致するドキュメントを検索
   */
  public Iterable<T> search(String keyword, int page, int size, String sortColumn);

  public T save(T model);

  public Iterable<T> save(Iterable<T> model);

  public void delete(String id);

  public void delete(Iterable<T> model);
}
```

**AbstractService**

各コレクション用のサービスクラスの基底クラスを`IService`インターフェースを実装して作成します。

この`AbstractService`クラスでは、似たような機能のメソッドが定義されていますが、その違いは次のようになっています。

* `IService`インターフェースのメソッドの実装では、上記で説明したrepositoryを使用します。
* `AbstractService`クラスで定義するメソッドは、MongoTemplateとQueryを使用するメソッドを使用します。

```java:AbstractService.java
package com.example.sbdm.service;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.repository.MongoRepository;

import com.example.sbdm.utils.MongoService;

public abstract class AbstractService<T> implements IService<T> {
  private static Logger logger = LoggerFactory.getLogger(AbstractService.class);

  @Autowired
  private MongoTemplate template;

  /* *
   * repositoryを使用する方法
   * 
   * */

  @Override
  public long count() {
    return getRepository().count();
  }

  @Override
  public T findById(String id) {
    return getRepository().findOne(id);
  }

  @Override
  public Iterable<T> findAll(int page, int size, String sort) {
    Pageable pager = new PageRequest(currentPage(page), size, Direction.ASC, sort);
    Iterable<T> result = getRepository().findAll(pager);
    return result;
  }

  @Override
  public T save(T model) {
    return getRepository().save(model);
  }

  @Override
  public Iterable<T> save(Iterable<T> model) {
    return getRepository().save(model);
  }

  @Override
  public void delete(String id) {
    getRepository().delete(id);
  }

  @Override
  public void delete(Iterable<T> model) {
    getRepository().delete(model);
  }

  abstract protected MongoRepository<T, String> getRepository();


  /* *
   * templateとqueryを使用する方法
   * 
   * */

  protected long doCount(Query query, Class<T> clazz) {
    return template.count(query, clazz);
  }

  protected T doFindOne(Query query, Class<T> clazz) {
    return template.findOne(query, clazz);
  }

  protected List<T> doFind(Query query, Class<T> clazz) {
    return template.find(query, clazz);
  }

  protected List<T> doFindAll(Class<T> clazz) {
    return template.findAll(clazz);
  }

  protected Criteria makeCriteriaById(String id) {
    return Criteria.where("id").is(id);
  }

  protected Criteria makeCriteriaRegex(Criteria criteria, String field, String param) {
    if (criteria == null) {
      criteria = Criteria.where(field).regex(param,"i");
    } else {
      criteria.and(field).regex(param,"i");
    }
    return criteria;
  }

  protected Criteria makeCriteria(Criteria criteria, String field, Object param) {
    if (criteria == null) {
      criteria = Criteria.where(field).is(param);
    } else {
      criteria.and(field).is(param);
    }
    return criteria;
  }

  protected Criteria makeWhere(String name) {
    return Criteria.where(name);
  }

  protected Criteria makeWhere(String name, Object param) {
    return Criteria.where(name).is(param);
  }

  protected Query makeQuery(Criteria criteria) {
    Query query;
    if (criteria != null) {
      query = new Query(criteria);
    } else {
      query = new Query();
    }
    return query;
  }

  protected int calcSkipNum(int page, int size) {
    return (page - 1) * size;
  }

  abstract protected long count(T searchCondition);
  abstract protected List<T> search(int page, int size, Sort sort, T searchCondition);
  abstract protected Criteria makeCriteriaByPk(T model);
  abstract protected Criteria makeCriteria(T model);
  abstract protected Update makeAllUpdate(T model);
}
```

**ポイント**

ドキュメントの操作方法には、Repositoryインタフェースを使用する方法と、MongoTemplateとQueryを使用する方法があります。どちらの方法でも同じことができますが、Repositoryを使用する方がコードの実装量が減ります。

* Repositoryを使用する方法: シンプルなクエリーであればメソッド定義だけで済み、コードの記述やクエリーを意識する必要がありません。
* MongoTemplateとQueryを使用する方法: プログラムでクエリーを組み立てることができます。

なおどちらの方法でも直接クエリーを実行することができます。


##### Serviceの具象クラス

パッケージ: `com.example.sbdm.service.impl`

**CustomersService**

```java:CustomersService.java
package com.example.sbdm.service.impl;

import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Service;

import com.example.sbdm.domain.Customers;
import com.example.sbdm.repository.CustomersRepository;
import com.example.sbdm.service.AbstractService;

@Service
public class CustomersService extends AbstractService<Customers> {
  private static Logger logger = LoggerFactory.getLogger(CustomersService.class);

  @Autowired
  private CustomersRepository customersRepository;

  @Override
  protected MongoRepository<Customers, String> getRepository() {
    return customersRepository;
  }

  @Override
  public Customers findByPk(Object...keys) {
    return customersRepository.findByCustomerNumber((Long)keys[0]);
  }

  @Override
  public Iterable<Customers> findByNameLike(String customerName, String sortColumn) {
    Sort sort = new Sort(sortColumn);
    return customersRepository.findByCustomerNameLike(customerName, sort);
  }

  @Override
  public long searchCount(String keyword) {
    return customersRepository.searchCount(keyword);
  }

  @Override
  public Iterable<Customers> search(String keyword, int page, int size, String sortColumn) {
    Pageable pager = new PageRequest(currentPage(page), size, Direction.ASC, sortColumn);
    return customersRepository.search(keyword, pager);
  }

  @Override
  public long count(Customers searchCondition) {
    Criteria criteria = makeCriteria(searchCondition);
    Query query = makeQuery(criteria);
    return doCount(query, Customers.class);
  }

  @Override
  public List<Customers> search(int page, int size, Sort sort, Customers searchCondition) {
    Criteria criteria = makeCriteria(searchCondition);
    Query query= makeQuery(criteria);
    query.skip(calcSkipNum(page, size)).limit(size);
    if (sort != null) {
      query.with(sort);
    }
    return doFind(query, Customers.class);
  }

  @Override
  protected Criteria makeCriteriaByPk(Customers model) {
    return Criteria.where("customerNumber").is(model.getCustomerNumber());
  }

  @Override
  protected Criteria makeCriteria(Customers model) {
    Criteria criteria = null;
    if (model.getCustomerNumber() != null && model.getCustomerNumber() > 0L) {
      criteria = makeCriteria(criteria, "customerNumber", model.getCustomerNumber());
    }
    if (StringUtils.isNotEmpty(model.getCustomerName())) {
      criteria = makeCriteria(criteria, "customerName", model.getCustomerName());
    }
    if (StringUtils.isNotEmpty(model.getPhone())) {
      criteria = makeCriteria(criteria, "phone", model.getPhone());
    }
    if (StringUtils.isNotEmpty(model.getCity())) {
      criteria = makeCriteria(criteria, "city", model.getCity());
    }
    if (StringUtils.isNotEmpty(model.getCountry())) {
      criteria = makeCriteria(criteria, "country", model.getCountry());
    }
    if (StringUtils.isNotEmpty(model.getState())) {
      criteria = makeCriteria(criteria, "state", model.getState());
    }
    if (StringUtils.isNotEmpty(model.getPostalCode())) {
      criteria = makeCriteria(criteria, "postalCode", model.getPostalCode());
    }
    return criteria;
  }

  @Override
  protected Update makeAllUpdate(Customers model) {
    Update update = new Update();
    update.set("customerName", model.getCustomerName());
    update.set("contactLastName", model.getContactLastName());
    update.set("contactFirstName", model.getContactFirstName());
    update.set("phone", model.getPhone());
    update.set("addressLine1", model.getAddressLine1());
    update.set("addressLine2",model.getAddressLine2());
    update.set("city", model.getCity());
    update.set("state", model.getState());
    update.set("postalCode", model.getPostalCode());
    update.set("country", model.getCountry());
    update.set("salesRepEmployeeNumber", model.getSalesRepEmployeeNumber());
    update.set("creditLimit", model.getCreditLimit());
    return update;
  }

}
```

#### Controller

パッケージ: `com.example.sbdm.web`

**CustomersController**

```java:CustomersController.java
package com.example.sbdm.web;

import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.sbdm.domain.Customers;
import com.example.sbdm.domain.Orders;
import com.example.sbdm.service.impl.CustomersService;
import com.example.sbdm.service.impl.OrdersService;
import com.example.sbdm.utils.JsonLoader;

@Controller
@RequestMapping(value = "/customers")
public class CustomersController extends BaseController {
  private static Logger logger = LoggerFactory.getLogger(CustomersController.class);

  private static final int PAGE_SIZE = 10;

  @Autowired
  private CustomersService customersService;

  @Autowired
  private OrdersService ordersService;

  @RequestMapping(method = RequestMethod.GET)
  public String _index(Model model) {
    return index(1, null, model);
  }

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public String index(Model model) {
    return index(1, null, model);
  }

  @RequestMapping(value = "/{pageNo}", method = RequestMethod.GET)
  public String index(
      @PathVariable Integer pageNo,
      @RequestParam String keyword,
      Model model) {
    logger.debug("CustomersController:[index] Passing through...");

    int totalCount = 0;
    Iterable<Customers> result;

    if (StringUtils.isNotEmpty(keyword)) {
      result = customersService.search(keyword, pageNo, PAGE_SIZE, "customerName");
      totalCount = (int)customersService.searchCount(keyword);
    } else {
      result = customersService.findAll(pageNo, PAGE_SIZE, "customerNumber");
      totalCount = (int)customersService.count();
    }

    model.addAttribute("keyword", keyword);
    model.addAttribute("result", result);

    addPageAttr(customersService.calcPage(totalCount, pageNo, PAGE_SIZE), model);

    return "Customers/index";
  }

  @RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)
  public String detail(
      @PathVariable String id,
      Model model) {
    logger.debug("CustomersController:[detail] Passing through...");

    Customers customer =  customersService.findById(id);

    String json = "{}";
    if (customer != null) {
      json = JsonLoader.toJson(customer);
    }

    Iterable<Orders> orderList = ordersService.findByCustomerNumber(customer.getCustomerNumber());

    model.addAttribute("customer", customer);
    model.addAttribute("orderList", orderList);
    model.addAttribute("json", json);

    return "Customers/detail";
  }

  @RequestMapping(value = "/search", method = RequestMethod.GET)
  public String search(
      @RequestParam(required = false) String country,
      @RequestParam(required = false) String city,
      @RequestParam(required = false) String state,
      @RequestParam(required = false) String postalcode,
      Model model) {
    logger.debug("CustomersController:[search] Passing through...");

    int pageNo = 1;

    Customers searchCondition = new Customers();
    if (StringUtils.isNotEmpty(country)) {
      searchCondition.setCountry(country);
    }
    if (StringUtils.isNotEmpty(city)) {
      searchCondition.setCity(city);
    }
    if (StringUtils.isNotEmpty(state)) {
      searchCondition.setState(state);
    }
    if (StringUtils.isNotEmpty(postalcode)) {
      searchCondition.setPostalCode(postalcode);
    }

    List<Customers> result = customersService.search(pageNo, PAGE_SIZE, null, searchCondition);

    int totalCount = (int)customersService.count(searchCondition);

    model.addAttribute("result", result);

    addPageAttr(customersService.calcPage(totalCount, pageNo, PAGE_SIZE), model);

    return "Customers/index";
  }

}
```

* `search`アクションの呼び出しは、ブラウザのアドレスバーに直接URLを入力して行います。実行結果は一覧ページに表示されます。


#### その他のクラスやテンプレート

その他のクラスやテンプレートについては省略します。(MongoDBに関係する重要な部分が無いことと、記事サイズが大きくなってしまうため。)
ソースコード全文はgithub上で公開しています。


### 実行する

MongoDBが起動されていることを確認し下記のコマンドを実行します。

```text
> mvn spring-boot:run
```

アプリケーションが起動したら下記のURLにアクセスしページが表示されれば成功です。
ただし、データが1件もない状態なので下図のページが表示されるはずです。

http://localhost:9000/

![empty.png](https://qiita-image-store.s3.amazonaws.com/0/22772/1e944a6d-ff2e-2a5d-0c5a-d3f5cc267a08.png)

#### データの初期化

初期データはjsonファイルで管理しています。(ファイルは`resources/data/init`フォルダ下にあります)

```text
resources
 └─data
    └─init
        ├─Customers
        ├─OrderDetails
        ├─Orders
        ├─Payments
        ├─ProductLines
        └─Products
```

ページのメニューにある"admin:init"をクリックするとjsonファイルをロードしてドキュメントを作成します。
データの初期化が成功すると下図の結果ページが表示されます。

![init_result.png](https://qiita-image-store.s3.amazonaws.com/0/22772/d63f6f46-59b2-b044-34f6-48e9d630dae3.png)

データの初期化が終わったら各ページでデータを確認することができます。

**顧客一覧**

![customers.png](https://qiita-image-store.s3.amazonaws.com/0/22772/aa586b8b-9c2f-dd4e-4992-1a956c5a5537.png)

**注文一覧**

![orders.png](https://qiita-image-store.s3.amazonaws.com/0/22772/914bd118-0fb5-5c0d-aa7d-067ccff1ff6a.png)

**製品一覧**

![products.png](https://qiita-image-store.s3.amazonaws.com/0/22772/0501ffb2-a59a-2b99-d794-dee23c8e8f6b.png)

**製品種目一覧**

![productlines.png](https://qiita-image-store.s3.amazonaws.com/0/22772/563c2ea7-0499-1855-0835-e0249d5a3dba.png)

**支払い一覧**

![payments.png](https://qiita-image-store.s3.amazonaws.com/0/22772/13b81834-10e1-1e60-220a-14237691b121.png)