Spring BootとSpring Data JPAで検索アプリケーションを開発してCircleCIでビルドする

# 概要

Spring-BootとSpring-Data-JPAを使用して検索アプリケーションを開発し、CircleCiでビルドを行うまでの手順のまとめです。
CircleCIではビルド結果をチャットサービスと連携して通知したり、指定する環境へデプロイすることもできるようですが、この記事ではそこまで触れません。


**環境**

Local開発環境

* Windows7 (64bit)
* Java 1.8.0_60
* Spring Boot 1.3.0.M5
 * Spring Boot Starter data JPA 1.3.0.M5
 * Spring Boot Starter tymeleaf 1.3.0.M5
* MySQL 5.6.26

CircleCIの設定 (circle.ymlの設定)

* 1 container
* Language Java 1.8
* Database MySQL 5.5


**参考**

* [CircleCI] (https://circleci.com/)
* [はじめての CircleCI] (http://www.slideshare.net/mogproject/circleci-51253223)

**GitHub**

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


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

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

プロジェクト名: sbci-example

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

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

```bat:eclipse
> cd sbci-example
> mvn eclipse:eclipse
```

**eclipseにインポート**

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

**pom.xmlの編集**

ビルド結果をレポート出力するためにmaven-surefire-report-plugin、cobertura-maven-pluginを使用します。

```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.sbci</groupId>
  <artifactId>sbci-example</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>sbci-example</name>
  <url>http://maven.apache.org</url>

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

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.0.M5</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-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-actuator</artifactId>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </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>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <repositories>
    <repository>
      <id>spring-libs-snapshot</id>
      <name>Spring</name>
      <url>http://repo.spring.io/libs-snapshot</url>
    </repository>
    <repository>
      <id>spring-snapshots</id>
      <name>Spring Snapshot</name>
      <url>http://repo.spring.io/snapshot</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
    <repository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>http://repo.spring.io/milestone</url>
      <snapshots>
          <enabled>false</enabled>
      </snapshots>
    </repository>
  </repositories>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <version>1.2.5.RELEASE</version>
        </plugin>
        <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.apache.maven.plugins</groupId>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.4</version>
          <configuration>
            <locales>ja</locales>
            <inputEncoding>${project.build.sourceEncoding}</inputEncoding>
            <outputEncoding>${site.encoding}</outputEncoding>
          </configuration>
        </plugin>
<!--
        <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>findbugs-maven-plugin</artifactId>
          <version>3.0.2</version>
          <configuration>
            <effort>Default</effort>
            <threshold>Default</threshold>
            <onlyAnalyze>com.example.sbci.*</onlyAnalyze>
            <xmlOutput>true</xmlOutput>
            Optional directory to put findbugs xdoc xml report
            <xmlOutputDirectory>target/site</xmlOutputDirectory>
          </configuration>
        </plugin>

-->
       </plugins>
    </pluginManagement>

    <plugins>
      <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>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
      </plugin>
<!--
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>findbugs-maven-plugin</artifactId>
      </plugin>

-->
    </plugins>
  </build>

  <reporting>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-report-plugin</artifactId>
        <reportSets>
          <reportSet>
            <reports>
              <report>report-only</report>
            </reports>
          </reportSet>
        </reportSets>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>cobertura-maven-plugin</artifactId>
        <configuration>
          <formats>
            <format>html</format>
            <format>xml</format>
          </formats>
        </configuration>
      </plugin>
<!--
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>findbugs-maven-plugin</artifactId>
      </plugin>

-->
    </plugins>
  </reporting>

</project>
```

**circle.ymlの作成**

プロジェクトのルートディレクトリにcircle.ymlを作成します。

```yml:circle.yml
machine:                               # (1)
  java:
    version: oraclejdk8
  environment:
    JAVA_OPTS: -Xms512m -Xmx512m
  timezone: Asia/Tokyo

general:
  branches:                             # (2)
    only:
      - master
  artifacts:                            # (3)
    - "target/surefire-reports"
    - "target/site"
    - "log"

database:                               # (4)
  post:
    - mysql -u root < $HOME/$CIRCLE_PROJECT_REPONAME/src/main/resources/data/sql/00-database.sql
    - mysql sample_db -u root < $HOME/$CIRCLE_PROJECT_REPONAME/src/main/resources/data/sql/01-schema.sql
    - mysql sample_db -u root < $HOME/$CIRCLE_PROJECT_REPONAME/src/main/resources/data/sql/02-data-customer.sql
    - mysql sample_db -u root < $HOME/$CIRCLE_PROJECT_REPONAME/src/main/resources/data/sql/03-data-orders.sql
    - mysql sample_db -u root < $HOME/$CIRCLE_PROJECT_REPONAME/src/main/resources/data/sql/04-data-order_detail.sql
    - mysql sample_db -u root < $HOME/$CIRCLE_PROJECT_REPONAME/src/main/resources/data/sql/05-data-payment.sql
    - mysql sample_db -u root < $HOME/$CIRCLE_PROJECT_REPONAME/src/main/resources/data/sql/06-data-product.sql
    - mysql sample_db -u root < $HOME/$CIRCLE_PROJECT_REPONAME/src/main/resources/data/sql/07-data-product_line.sql

test:
  override:                              # (5)
    - mvn site
```

|No  |Description                                                                            |
|:--:|:--------------------------------------------------------------------------------------|
|1   |VMの設定を行います。                                                                    |
|2   |checkoutするブランチを指定します。                                                       |
|3   |artifactsで指定するディレクトリの内容がaritifactsとしてビルド後に参照できるように指定します。|
|4   |テストで使用するデータベース、スキーマ、アカウント、テストデータの作成を行うsqlを実行します。 |
|5   |テスト結果のレポートを得るためにテストコマンドをmvn siteに変えます。(デフォルトはmvn integration-test) |


***artifactについて***

aritifactsセクションで指定するディレクトリは下図のようにArtifactsページで参照できます。
この例ではmvn siteで生成するレポートが見られるように指定しています。

![c05.png](https://qiita-image-store.s3.amazonaws.com/0/22772/40e049dd-e70d-c666-ba30-00541141de37.png)


**resourcesディレクトリの作成**

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

作成したresourcesディレクトリをプロジェクトに反映させます。

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


**templatesディレクトリの作成**

テンプレートファイルを配置するtemplatesディレクトリをsrc/main/resources下に作成します。


**application.ymlの作成**

src/main/resourcesディレクトリにapplication.ymlを作成します。

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

spring:
# THYMELEAF (ThymeleafAutoConfiguration)
  thymeleaf:
    enabled: true
    cache: false
# DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost/sample_db
    username: test_user
    password: test_user
    jmx-enabled: true
# JPA (JpaBaseConfiguration, HibernateJpaAutoConfiguration)
  jpa:
    hibernate:
      show-sql: true
      ddl-auto: update
    database-platform: org.hibernate.dialect.MySQLDialect
  data:
    jpa:
      repositories:
        enabled: true
# INTERNATIONALIZATION (MessageSourceAutoConfiguration)
  messages:
    basename: messages
    cache-seconds: -1
    encoding: UTF-8

# ENDPOINTS (AbstractEndpoint subclasses)
endpoints:
  enabled: true

# JMX ENDPOINT (EndpointMBeanExportProperties)
  jmx:
    enabled: true
    domain: com.example.sbci
```


**logback.xmlの作成**

src/main/resourcesディレクトリにlogback.xmlを作成します。
ログファイルの出力先はデフォルトではアプリケーションの実行ディレクトリ下のlogディレクトリに出力されます。ログの出力先は環境変数log.dirで変えることができます。

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

<!--   <property name="LOG_DIR" value="${log.dir:-D:/logs}" /> -->
  <property name="LOG_DIR" value="${log.dir:-./log}" />

  <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}/sbci-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="INFO"/>
  <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>
```

JPAプロバイダにHibernateを使用しているので下記のloggerのレベルをDEBUGにすると、JPAが実際に発行するネイティブSQLなどを確認することができます。

```xml:org.hibernate
<logger name="org.hibernate" level="INFO"/>
```


**ビルド**

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

```bat:package
> mvn package
```

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

```bat:jar
> cd target
> java -jar sbci-example-1.0-SNAPSHOT.jar
Hello World!
```

#### テストデータの準備

テストに使用するデータは下記の通りです。

* データベース名: sample_db
* アカウント: test_user/test_user

**テーブル**

* customer : カスタマーテーブル
* orders : 注文テーブル
* order_detail : 注文明細テーブル
* payment : 支払いテーブル
* product : 製品テーブル
* product_line : 製品種別テーブル

**sqlファイル**

テストに使用するデータベースなどを作成するsql文をsrc/main/resource/data/sqlディレクトリに配置します。
ここに配置するsqlファイルはCircleCIでデータベース環境の構築に使用します。

* 00-database.sql : データベース、アカウントを作成するsql
* 01-schema.sql : テーブルを作成するsql
* 02-data-customer.sql : カスタマー
* 03-data-orders.sql : 注文
* 04-data-order_detail.sql : 注文明細
* 05-data-payment.sql : 支払い
* 06-data-product.sql : 製品
* 07-data-product_line.sql : 製品種別

***00-database.sql***

```sql:00-database.sql
create database if not exists sample_db character set utf8;
create user 'test_user'@'localhost' identified by 'test_user';
grant all on sample_db.* to 'test_user'@'localhost';
```

***01-schema.sql***

```sql:01-schema.sql
drop table if exists customer;
drop table if exists orders;
drop table if exists order_detail;
drop table if exists payment;
drop table if exists product;
drop table if exists product_Line;


create table if not exists customer (
  id int not null auto_increment,
  customer_number int not null,
  customer_name varchar(124) not null,
  contact_last_name varchar(124) not null,
  contact_first_name varchar(124) not null,
  phone varchar(32),
  address_line1 varchar(124),
  address_line2 varchar(124),
  city varchar(32),
  state varchar(32),
  postal_code varchar(32),
  country varchar(32),
  sales_rep_employee_number int,
  credit_limit decimal(10,2),
  primary key (id)
) engine = INNODB;

create table if not exists orders (
  id int not null auto_increment,
  order_number int not null,
  order_date date not null,
  required_date date not null,
  shipped_date date,
  status varchar(32),
  comments varchar(256),
  customer_number int,
  primary key (id)
) engine = INNODB;

create table if not exists order_detail (
  id int not null auto_increment,
  order_number int not null,
  product_code varchar(32) not null,
  quantity_ordered int,
  price_each decimal(10,2),
  order_line_number int,
  primary key (id)
) engine = INNODB;

create table if not exists payment (
  id int not null auto_increment,
  customer_number int not null,
  check_number varchar(64) not null,
  payment_date date not null,
  amount decimal(10,2) not null,
  primary key (id)
) engine = INNODB;

create table if not exists product (
  id int not null auto_increment,
  product_code varchar(32) not null,
  product_name varchar(128) not null,
  product_line varchar(32) not null,
  product_scale varchar(32) not null,
  product_vendor varchar(64) not null,
  product_description varchar(1024),
  quantity_in_stock int,
  buy_price decimal(10,2),
  msrp decimal(10,2),
  primary key (id)
) engine = INNODB;

create table if not exists product_line (
  id int not null auto_increment,
  product_line varchar(32) not null,
  text_description varchar(1024),
  html_description varchar(1024),
  image varchar(1024),
  primary key (id)
) engine = INNODB;
```

***02-data-customer.sql (一部)***

```sql:02-data-customer.sql
insert into customer (customer_number, customer_name, contact_last_name, contact_first_name, phone, address_line1, address_line2, city, state, postal_code, country, sales_rep_employee_number, credit_limit) values
(103, "Atelier graphique", "Schmitt", "Carine", "40.32.2555", "54, rue Royale", null, "Nantes", null, "44000", "France", 1370, 21000),
(112, "Signal Gift Stores", "King", "Jean", "7025551838", "8489 Strong St.", null, "Las Vegas", "NV", "83030", "USA", 1166, 71800),
(114, "Australian Collectors, Co.", "Ferguson", "Peter", "03 9520 4555", "636 St Kilda Road", "Level 3", "Melbourne", "Victoria", "3004", "Australia", 1611, 117300),

...省略...
```



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

#### App

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

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

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.example.sbci.viewhelper.MyDialect;

@SpringBootApplication
@EntityScan(basePackages = {"com.example.sbci.domain"})                 // (1)
@EnableJpaRepositories(basePackages = {"com.example.sbci.repository"})  // (2)
@EnableTransactionManagement(proxyTargetClass = true)                   // (3)
public class App {
  public static void main( String[] args ) {
    SpringApplication.run(App.class, args);
  }

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

}
```

|No  |Description                                                          |
|:--:|:--------------------------------------------------------------------|
|1   |Domainクラスを格納するパッケージ名を指定します。                        |
|2   |Repositoryクラスを格納するパッケージ名を指定します。                    |
|3   |トランザクションを有効にします。                                       |
|4   |Thymeleafテンプレートで使用するユーティリティオブジェクトを登録します。  |


#### Domain

データベースのテーブルに対応するエンティティクラスを作成します。

```java:Orders
package com.example.sbci.domain;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.validation.constraints.NotNull;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

@Entity                                              // (1)
@Table(name = "orders")                              // (2)
public class Orders implements Serializable {

  private static final long serialVersionUID = 3744346731479843943L;

  @Id                                                // (3)
  @GeneratedValue(strategy=GenerationType.IDENTITY)  // (4)
  private Long id;
  @NotNull                                           // (5)
  @Column(name="order_number", nullable=false)       // (6)
  private Long orderNumber;
  @NotNull
  @Temporal(TemporalType.DATE)                       // (7)
  @Column(name="order_date", nullable=false)
  private Date orderDate;
  @NotNull
  @Temporal(TemporalType.DATE)
  @Column(name="required_date", nullable=false)
  private Date requiredDate;
  @Temporal(TemporalType.DATE)
  @Column(name="shipped_date")
  private Date shippedDate;
  @Column(name="status")
  private String status;
  @Column(name="comments")
  private String comments;
  @Column(name="customer_number")
  private Long customerNumber;

  ...getter/setterは省略...
}
```

|No  |Description                                                                            |
|:--:|:--------------------------------------------------------------------------------------|
|1   |クラスにEntityアノテーションを付ける事でJPAで管理する対象とします。 |
|2   |Tableアノテーションでクラス名とデータベースのテーブル名のデフォルトのマッピングを上書きします。 |
|3   |フィールドにIdアノテーションを付ける事でこのフィールドを主キーとします。|
|4   |GeneratedValueアノテーションで主キーの生成方法を指定します。 |
|5   |このアノテーションはBean Validation(JSR 303)のものです。JPAではエンティティ更新時のデータの妥当性検査にBean Validation APIを使用します。 |
|6   |Columnアノテーションでフィールド名とデータベースのフィールド名のデフォルトのマッピングを上書きします。 |
|7   |フィールドがjava.util.Dateおよびjava.util.Calendarの場合、マッピングするクラスを指定します。 |

**strategy**

|Type      |Description                  |
|:---------|:----------------------------|
|AUTO      |JPAが適切な主キーの生成方法を選択します。(使用するデータベースに応じてIDENTITY,SEQUENCE,TABLEが選択されます) |
|IDENTITY  |データベース固有の機能で主キーを生成します。(MySQLの場合はauto_incrementを使用) |
|SEQUENCE  |Sequenceを使用して主キーの値を生成します。 SequenceGeneratorアノテーションでシーケンス名などを指定できます。|
|TABLE     |Tableを使用して主キーの値を生成します。TableGeneratorアノテーションでテーブル名などを指定できます。 |

**TemporalType**

|Type      |Description                  |
|:---------|:----------------------------|
|DATE      |java.sql.Date                |
|TIME      |java.sql.Time                |
|TIMESTAMP |java.sql.Timestamp           |


#### Repository

RepositoryはSpring Data JPAの機能でJPAのAPIをラップしています。
Repositoryを使うことでJPAが管理するエンティティを操作することができます。

```java:OrdersRepository
package com.example.sbci.repository;

import java.util.Date;
import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import com.example.sbci.domain.Orders;

@Repository
public interface OrdersRepository extends JpaRepository<Orders, Long> {

  @Query(value = "select * from orders where order_number = :orderNumber limit 1", nativeQuery = true)  // (1)
  //@Query(value = "select o from Orders o where o.orderNumber = :orderNumber")   // (2)
  Orders findByPk(@Param("orderNumber") Long orderNumber);

  @Query(value = "select customerNumber from Orders where id = :id")              // (3)
  Long getCustomerNumber(@Param("id") Long id);

  @Query(name = "Orders.findByOrderDateRange")                                    // (4)
  List<Orders> findByOrderDateRange(@Param("from") Date from, @Param("to") Date to);

  @Modifying(clearAutomatically = true)                                           // (5)
  @Query(value = "update Orders o set o.comments = :comments where o.orderNumber = :orderNumber")
  Integer updateComments(@Param("orderNumber") Long orderNumber, @Param("comments") String comments);

}
```

|No  |Description                                                                            |
|:--:|:--------------------------------------------------------------------------------------|
|1   |ネイティブSQLを実行します。                                                              |
|2   |コメントされたこの行は上の行のネイティブSQLをJPQLで記述したものです。                       |
|3   |JPQL(Java Persistence Query Language)で記述したSQLを実行します。                         |
|4   |プロパティファイルにJPQLで記述したSQLを実行します。                                        |
|5   |実行するクエリがエンティティを更新することをJPAに伝えます。                                 |

***JPQL***

JPQL(Java Persistence Query Language)はJPAで標準化されているSQLに似た構文を持つ問い合わせ言語です。
データベースに依存しないクエリを発行することができます。

***プロパティファイル***

JPQLをプロパティファイルに管理することができます。
デフォルトではMETA-INFディレクトリにjpa-named-queries.propertiesという名前のファイルで作成します。
この例では、src/main/resources/META-INF/jpa-named-queries.propertiesで作成しました。

```text:jpa-named-queries.properties
# Orders SQL
Orders.findByOrderDateRange = select o from Orders o where o.orderDate >= :from and o.orderDate <= :to order by o.orderDate asc

# Product SQL
Product.findByDescriptionOrderByProductName = select p from Product p where p.productDescription like :desc order by p.productName asc
```


#### Service

```java:OrdersService.java
package com.example.sbci.service;

import java.util.Date;
import java.util.List;

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.Direction;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.sbci.domain.Orders;
import com.example.sbci.repository.OrdersRepository;

@Service
public class OrdersService implements Pagination {

  @Autowired
  OrdersRepository ordersRepository;

  @Transactional(readOnly = true, timeout = 3)                                   // (1)
  public Orders findById(final Long id) {
    return ordersRepository.findOne(id);
  }

  @Transactional(readOnly = true, timeout = 3)
  public Orders findByPk(final Long orderNumber) {
    return ordersRepository.findByPk(orderNumber);
  }

  @Transactional(readOnly = true, timeout = 10)
  public Iterable<Orders> findAll(int page, int size, String sort) {
    Pageable pager = new PageRequest(currentPage(page), size, Direction.ASC, sort);
    return ordersRepository.findAll(pager);
  }

  @Transactional(readOnly = true, timeout = 10)
  public List<Orders> findByOrderDateRange(Date from, Date to) {
    return ordersRepository.findByOrderDateRange(from, to);
  }

  @Transactional(rollbackFor = {Exception.class}, timeout = 3)                   // (2)
  public Orders save(final Orders order) {
    return ordersRepository.save(order);
  }

  @Transactional(rollbackFor = {Exception.class}, timeout = 10)
  public List<Orders> saveAll(final Iterable<Orders> orders) {
    return ordersRepository.save(orders);
  }

  @Transactional(rollbackFor = {Exception.class}, timeout = 3)
  public void remove(final Orders order) {
    ordersRepository.delete(order);
  }

  @Transactional(rollbackFor = {Exception.class}, timeout = 10)
  public void removeAll(final Iterable<Orders> orders) {
    ordersRepository.delete(orders);
  }

  @Transactional(rollbackFor = {Exception.class}, timeout = 3)
  public Integer updateComments(final Long orderNumber, final String comments) {
    return ordersRepository.updateComments(orderNumber, comments);
  }

}
```

|No  |Description                                                                            |
|:--:|:--------------------------------------------------------------------------------------|
|1   |読み取り専用のトランザクションとし、タイムアウト(秒)を3秒に指定します。                     |
|2   |Exceptionがスローされた場合にロールバックします。またタイムアウト(秒)を3秒に指定します。     |


#### Controller

```java:OrdersController.java
package com.example.sbci.web;

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 com.example.sbci.domain.Orders;
import com.example.sbci.service.OrdersService;

@Controller
@RequestMapping(value = "/orders")
public class OrdersController {
  final static Logger logger = LoggerFactory.getLogger(OrdersController.class);

  @Autowired
  OrdersService ordersService;

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

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public String index(Model model) {
    logger.debug("OrdersController:[index] Passing through...");
    Iterable<Orders> result = ordersService.findAll(1, 10, "id");
    model.addAttribute("result", result);
    return "Orders/index";
  }

  @RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)
  public String detail(@PathVariable("id") Long id, Model model) {
    logger.debug("OrdersController:[detail] Passing through...");
    Orders order = ordersService.findById(id);
    model.addAttribute("order", order);
    return "Orders/detail";
  }

}
```


### テストケースの開発

#### Repositoryクラスのテスト

```java:OrdersRepositoryTest.java
package com.example.sbci.repository;

import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;

import java.util.Date;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import com.example.sbci.App;
import com.example.sbci.DateHelper;
import com.example.sbci.domain.Orders;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = App.class)
public class OrdersRepositoryTest {

  @Autowired
  OrdersRepository orderRepository;

  @Test
  public void executeQueryCount() {
    Long count = orderRepository.count();
    assertThat(count, is(326L));
  }

  @Test
  public void executeQueryFindOne() {
    Long id = 326L;
    Orders order = orderRepository.findOne(id);
    assertThat(order, notNullValue());
    assertThat(order.getId(), is(id));
  }

  @Test
  public void executeQueryFindByPk() {
    Long orderNumber = 10425L;
    Orders order = orderRepository.findByPk(orderNumber);
    assertThat(order, notNullValue());
    assertThat(order.getOrderNumber(), is(orderNumber));
  }

  @Test
  public void executeQueryFindAll() {
    List<Orders> list = orderRepository.findAll();
    assertThat(list, notNullValue());
    assertThat(list.size(), is(326));
  }

  @Test
  public void executeQueryFindByOrderDateRange() {
    Date from = DateHelper.parse("2013-05-01");
    Date to = DateHelper.parse("2013-05-31");
    List<Orders> list = orderRepository.findByOrderDateRange(from, to);
    assertThat(list, notNullValue());
    assertThat(list.size(), is(15));
  }

  @Test
  @Transactional
  public void executeUpdateComments() {
    Long orderNumber = 10245L;

    String c1 = "test update comment du3hB8ajwO";

    Integer u1 = orderRepository.updateComments(orderNumber, c1);
    assertThat(u1, is(1));

    Orders o1 = orderRepository.findByPk(orderNumber);
    assertThat(o1, notNullValue());
    assertThat(o1.getComments(), is(c1));


    String c2 = "test update comment vP49ayRjfy";

    Integer u2 = orderRepository.updateComments(orderNumber, c2);
    assertThat(u2, is(1));

    Orders o2 = orderRepository.findByPk(orderNumber);
    assertThat(o2, notNullValue());
    assertThat(o2.getComments(), is(c2));
  }

}
```


#### Serviceクラスのテスト

```java:OrdersServiceTest.java
package com.example.sbci.service;

import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;

import java.util.Arrays;
import java.util.List;

import javax.validation.ValidationException;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.dao.DataAccessException;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.example.sbci.App;
import com.example.sbci.DateHelper;
import com.example.sbci.domain.Orders;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = App.class)
public class OrdersServiceTest {

  @Autowired
  OrdersService ordersService;

  Orders order1;
  Orders order2;
  Orders order3;
  Orders order4;
  Orders order5;

  @Before
  public void setup() {
    order1 = new Orders();
    order1.setOrderNumber(555L);
    order1.setOrderDate(DateHelper.parse("2015-09-23"));
    order1.setRequiredDate(DateHelper.parse("2015-09-27"));
    order1.setShippedDate(DateHelper.parse("2015-09-30"));
    order1.setStatus("Shipped");
    order1.setComments(null);
    order1.setCustomerNumber(1111L);

    order2 = new Orders();
    order2.setOrderNumber(555L);
    order2.setOrderDate(DateHelper.parse("2015-09-23"));
    order2.setRequiredDate(DateHelper.parse("2015-09-27"));
    order2.setShippedDate(DateHelper.parse("2015-09-30"));
    order2.setStatus("Shipped");
    order2.setComments(null);
    order2.setCustomerNumber(1111L);

    order3 = new Orders();
    order3.setOrderNumber(555L);
    order3.setOrderDate(DateHelper.parse("2015-09-23"));
    order3.setRequiredDate(DateHelper.parse("2015-09-27"));
    order3.setShippedDate(DateHelper.parse("2015-09-30"));
    order3.setStatus("Shipped");
    order3.setComments(null);
    order3.setCustomerNumber(1111L);

    order4 = new Orders();
    order4.setOrderNumber(null);

    order5 = new Orders();
    order5.setOrderNumber(555L);
    order5.setOrderDate(DateHelper.parse("2015-09-23"));
    order5.setRequiredDate(DateHelper.parse("2015-09-27"));
    order5.setShippedDate(DateHelper.parse("2015-09-30"));
    order5.setStatus("1234567890123456789012345678901234567890");
    order5.setComments(null);
    order5.setCustomerNumber(1111L);

  }

  @After
  public void tearDown() {
    clear();
  }

  private void clear() {
    ordersService.removeAll(Arrays.asList(order1, order2, order3));
  }

  @Test
  public void save_ok() {
    Orders result = ordersService.save(order1);
    assertThat(result, notNullValue());
    Orders actual = ordersService.findById(result.getId());
    assertThat(actual, is(result));

    order1.setId(actual.getId());
  }

  @Test
  public void saveAll_ok() {
    List<Orders> orders = ordersService.saveAll(Arrays.asList(order2, order3));
    assertThat(orders, notNullValue());

    Orders actual2 = ordersService.findById(orders.get(0).getId());
    Orders actual3 = ordersService.findById(orders.get(1).getId());
    assertThat(actual2, is(orders.get(0)));
    assertThat(actual3, is(orders.get(1)));

    order2.setId(actual2.getId());
    order3.setId(actual3.getId());
  }

  @Test(expected = ValidationException.class)
  public void save_null_ng() {
    try {
      ordersService.save(order4);
    } catch (ValidationException e) {
      System.out.println("DataAccessException!!");
      System.out.println("message:" + e.getMessage());
      throw e;
    }
    org.junit.Assert.fail();
  }

  @Test(expected = DataAccessException.class)
  public void save_too_long_ng() {
    try {
      ordersService.save(order5);
    } catch (DataAccessException e) {
      System.out.println("DataIntegrityViolationException!!");
      System.out.println("message:" + e.getMessage());
      throw e;
    }
    org.junit.Assert.fail();
  }

  @Test
  public void remove_ok() {
    Orders result = ordersService.save(order1);
    assertThat(result, notNullValue());

    ordersService.remove(result);

    Orders order = ordersService.findById(result.getId());
    assertThat(order, nullValue());
  }

  @Test
  public void removeAll_ok() {
    List<Orders> orders = ordersService.saveAll(Arrays.asList(order2, order3));
    assertThat(orders, notNullValue());

    ordersService.removeAll(Arrays.asList(order2, order3));

    Orders order2 = ordersService.findById(orders.get(0).getId());
    Orders order3 = ordersService.findById(orders.get(1).getId());
    assertThat(order2, nullValue());
    assertThat(order3, nullValue());
  }

  @Test
  public void updateComments_ok() {
    Long orderNumber = 10245L;

    String c1 = "test update comment du3hB8ajwO";

    Integer u1 = ordersService.updateComments(orderNumber, c1);
    assertThat(u1, is(1));

    Orders o1 = ordersService.findByPk(orderNumber);
    assertThat(o1, notNullValue());
    assertThat(o1.getComments(), is(c1));


    String c2 = "test update comment vP49ayRjfy";

    Integer u2 = ordersService.updateComments(orderNumber, c2);
    assertThat(u2, is(1));

    Orders o2 = ordersService.findByPk(orderNumber);
    assertThat(o2, notNullValue());
    assertThat(o2.getComments(), is(c2));

  }

}
```


#### Controllerクラスのテスト

```java:OrdersControllerTest.java
package com.example.sbci.web;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import com.example.sbci.App;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = App.class)
@WebAppConfiguration
public class OrdersControllerTest {

  @Autowired
  private WebApplicationContext context;

  private MockMvc mvc;

  @Before
  public void before() throws Exception {
    this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build();
  }

  @Test
  public void testIndexGet_Ok() throws Exception {
    this.mvc.perform(get("/orders"))
       .andExpect(status().isOk())
       .andExpect(content().contentType("text/html;charset=UTF-8"))
       .andExpect(content().string(containsString("<title>sbci-example - orders</title>")));
  }

  @Test
  public void testDetailGet_Ok() throws Exception {
    this.mvc.perform(get("/orders/detail/100"))
    .andExpect(status().isOk())
    .andExpect(content().contentType("text/html;charset=UTF-8"))
    .andExpect(content().string(containsString("<h3>detail - contents</h3>")));
  }

  @Test
  public void testDetailGet_NotFound() throws Exception {
    this.mvc.perform(get("/orders/notfound"))
    .andExpect(status().is4xxClientError());
  }

}
```


#### Appクラスのテスト

```java:AppTest.java
package com.example.sbci;

import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;

import org.junit.Rule;
import org.junit.Test;
import org.springframework.boot.test.OutputCapture;

public class AppTest {

  @Rule
  public OutputCapture outputCapture = new OutputCapture();            // (1)

  @Test
  public void testCommandLineOverrides() throws Exception {
    App.main(new String[]{});
    String output = this.outputCapture.toString();
    assertThat(output.contains("Started App"), is(Boolean.TRUE));
    assertThat(output.contains("Exception"), is(Boolean.FALSE));
  }

}
```

|No  |Description                                                                            |
|:--:|:--------------------------------------------------------------------------------------|
|1   |OutputCaptureはSystem.outおよびSystem.errの出力をキャプチャすることができます。           |



##### テストの実行

mvn testでテストを実行しパスすることを確認します。

```bat
> mvn test

...省略...

Results :

Tests run: 47, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 15.198 s
[INFO] Finished at: 2015-09-29T16:52:36+09:00
[INFO] Final Memory: 17M/268M
[INFO] ------------------------------------------------------------------------
```



### GitHub

#### リモートリポジトリ

GitHubにリモートリポジトリを作成します。

リポジトリ名: `sbci-example`


#### ローカルリポジトリ

プロジェクトのルートディレクトリに.gitignoreファイルを作成します。

```text:.gitignore
/target/
/.settings/
/log/*.log
.project
.classpath
```

プロジェクトをgitリポジトリ化します。
git bashを立ち上げて下記のコマンドを実行します。

```bash:init
> git init
```

```bash:add
> git add .
```

```bash:commit
> git commit -m "first commit"
```

```bash:remote
> git remote add origin git@github.com:rubytomato/sbci-example.git
```

```bash:push
> git push -u origin master
```

**eclipseの設定**

リモートリポジトリを追加します。

Git Perspectiveを開き、Git Repositories Viewの"Add an existing local Git Repository to this view"ボタンをクリックします。
プロジェクトをディレクトリを選択する画面が表示されるのでsbci-exampleプロジェクトのディレクトリを選択します。


## CircleCI

[CircleCI] (https://circleci.com/)にアクセスしGitHubアカウントでログインします。

### 設定

ビルドするプロジェクトの設定を行います。
画面左側の"Add Projects"アイコンをクリックします。

![c01.png](https://qiita-image-store.s3.amazonaws.com/0/22772/4705f921-b818-ef24-0367-d3b021cd0c9a.png)

(1)Your accountsのラベルをクリックすると(2)の欄にリポジトリの一覧が表示されます。
その中から"sbci-example"プロジェクトのBuild Projectボタンをクリックします。

![c02.png](https://qiita-image-store.s3.amazonaws.com/0/22772/68119dbe-be60-b928-6b93-c9331ff36de7.png)

すぐにテスト環境の構築とテストの経過がリアルタイムに表示されます。

![c06.png](https://qiita-image-store.s3.amazonaws.com/0/22772/98c11723-c981-faec-1d89-26fa80a4d560.png)

この設定が終わった後は、次からGithubへpushすると自動的にビルドが行われます。

なお、ビルドを止めたい場合はプロジェクトの設定ページを開き、左側のメニューのProject Settings -> Overviewをクリックし、"Stop Building on Circle"ボタンをクリックします。

![c08.png](https://qiita-image-store.s3.amazonaws.com/0/22772/1ab1f60a-267e-aa73-1225-1d68e6dceb61.png)


### その他の設定

#### ステータスバッジをREADMEに表示する

プロジェクトの設定ページを開き、左側のメニューのPermissions -> API Permissionsをクリックします。
スコープに"Status"を選びラベルに"badge"と入力（任意の文字列)し、Create Tokenボタンをクリックします。

![c03.png](https://qiita-image-store.s3.amazonaws.com/0/22772/53907b1f-84bf-2641-b283-ec68f7a53177.png)

タグを生成するされるのでその内容をREADME.mdに記載します。

![c04.png](https://qiita-image-store.s3.amazonaws.com/0/22772/4cce133f-9ff2-1767-7c15-d5366f14c743.png)

READMEにこのようなバッジが表示されます。

![c07.png](https://qiita-image-store.s3.amazonaws.com/0/22772/69219236-ed6e-aeda-41c9-4d58934c596f.png)


#### 環境変数の登録

circle.ymlに記述できないトークンやAPIキーなどはプロジェクトの環境変数に登録することでビルド時に利用できます。
プロジェクトの設定ページを開き、左側のメニューのTweaks -> Enviroment variablesをクリックします。