概要
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でアプリケーションの雛形を作成
> mvn archetype:generate -DgroupId=com.example.sbci -DartifactId=sbci-example -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
> cd sbci-example
> mvn eclipse:eclipse
eclipseにインポート
- メニューバーの"File" -> "Import..." -> "Maven" -> "Existing Maven Projects"を選択します。
- プロジェクトのディレクトリを選択し、"Finish"ボタンをクリックします。
pom.xmlの編集
ビルド結果をレポート出力するためにmaven-surefire-report-plugin、cobertura-maven-pluginを使用します。
<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を作成します。
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で生成するレポートが見られるように指定しています。
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を作成します。
# 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 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などを確認することができます。
<logger name="org.hibernate" level="INFO"/>
ビルド
この時点で動作検証を兼ねてビルドします。
> mvn package
ビルドが成功したら生成したjarファイルを実行します。
コマンドプロンプトに"Hello World!"と表示されれば成功です。
> 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
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
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 (一部)
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がありますので、このファイルを下記のように変更します。
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
データベースのテーブルに対応するエンティティクラスを作成します。
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が管理するエンティティを操作することができます。
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で作成しました。
# 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
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
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クラスのテスト
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クラスのテスト
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クラスのテスト
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クラスのテスト
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でテストを実行しパスすることを確認します。
> 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ファイルを作成します。
/target/
/.settings/
/log/*.log
.project
.classpath
プロジェクトをgitリポジトリ化します。
git bashを立ち上げて下記のコマンドを実行します。
> git init
> git add .
> git commit -m "first commit"
> git remote add origin git@github.com:rubytomato/sbci-example.git
> 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"アイコンをクリックします。
(1)Your accountsのラベルをクリックすると(2)の欄にリポジトリの一覧が表示されます。
その中から"sbci-example"プロジェクトのBuild Projectボタンをクリックします。
すぐにテスト環境の構築とテストの経過がリアルタイムに表示されます。
この設定が終わった後は、次からGithubへpushすると自動的にビルドが行われます。
なお、ビルドを止めたい場合はプロジェクトの設定ページを開き、左側のメニューのProject Settings -> Overviewをクリックし、"Stop Building on Circle"ボタンをクリックします。
その他の設定
ステータスバッジをREADMEに表示する
プロジェクトの設定ページを開き、左側のメニューのPermissions -> API Permissionsをクリックします。
スコープに"Status"を選びラベルに"badge"と入力(任意の文字列)し、Create Tokenボタンをクリックします。
タグを生成するされるのでその内容をREADME.mdに記載します。
READMEにこのようなバッジが表示されます。
環境変数の登録
circle.ymlに記述できないトークンやAPIキーなどはプロジェクトの環境変数に登録することでビルド時に利用できます。
プロジェクトの設定ページを開き、左側のメニューのTweaks -> Enviroment variablesをクリックします。