SpringBoot(SPA)+MyBatis+RDBMS(PostgreSQL)


タイトルの組み合わせで動作するアプリを作りました。
フォルダ構成と、用意すべきクラスは自分の理解の範疇で設定しています。
レイヤー構造ってどこまで用意すればいいんだろうなー。


# フォルダ構成

```
アプリフォルダ
│
├─build.gradle
│
├─src/main
│  ├─java
│  │  └─com
│  │      └─example
│  │          │  SpaTestApplication.java : SpringBootApp
│  │          │  
│  │          ├─config
│  │          │      MyConfiguration.java : JavaConfig
│  │          │      
│  │          ├─domain
│  │          │      Item.java : 業務データモデル
│  │          │      
│  │          ├─dto
│  │          │      DtoItem.java : DBレコード
│  │          │      
│  │          ├─mapper
│  │          │      ItemMapper.java : （テーブルとSQLの？）マッパー
│  │          │      
│  │          ├─repository
│  │          │      ItemRepository.java : ドメインとDTOの間を吸収する
│  │          │      
│  │          ├─service
│  │          │      ItemService.java : ドメインを使ってビジネスロジックを処理する
│  │          │      
│  │          └─web
│  │              └─rest
│  │                      ItemResource.java : RESTコントローラ
│  │                      
│  └─resources
│      ├─config
│      │      mybatis.xml : MyBatisの設定内容
│      │      
│      ├─sql
│      │      ItemMapper.xml : ItemMapperに対応するSQL群
│      │      
│      └─static
│          │  index.html : ページ
│          │  
│          ├─css
│          │      （割愛）
│          │      
│          ├─js
│          │      （割愛）
│          │      
│          └─vendor
│              ├─bootstrap
│              │      （割愛）
│              │          
│              ├─Ink
│              │      （割愛）
│              │          
│              └─jquery
│                     （割愛）
│                      
└─src/test
    └─java
        └─com
            └─example
                      （割愛）
```
# Gradle

この記事として必要なものは

- org.mybatis.spring.boot:mybatis-spring-boot-starter:1.1.1
- org.postgresql:postgresql:9.4.1209
- org.springframework.boot:spring-boot-starter-web

くらいかな？

```groovy:build.gradle
buildscript {
	ext {
		springBootVersion = '1.4.0.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'

jar {
	baseName = 'demo'
	version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
	mavenCentral()
}


dependencies {
    compile "javax.inject:javax.inject:${javax_inject_version}"


    // DBまわり
    compile('org.springframework:spring-tx:4.1.5.RELEASE')
    compile('org.springframework:spring-jdbc:4.1.5.RELEASE')
	compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.1.1')

	compile('org.postgresql:postgresql:9.4.1209')

	// 必須
	compile('org.projectlombok:lombok')
	compile('org.springframework.boot:spring-boot-starter-web')

	testCompile('org.springframework.boot:spring-boot-starter-test')
}


eclipse {
	classpath {
		 containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
		 containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
	}
}

bootRun {
	addResources = true
}

task wrapper(type: Wrapper) {
    gradleVersion = '3.0'
}
```

# SPA部分

## アプリ

```java:SpaTestApplication.java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpaTestApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpaTestApplication.class, args);
	}
}
```

## RESTコントローラ

```java:SpaTestApplication.java
package com.example.web.rest;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import com.example.domain.Item;
import com.example.service.ItemService;
import org.springframework.web.bind.annotation.*;

import javax.inject.Inject;

@RestController
@RequestMapping("/api")
public class ItemResource {

	@Inject
	private ItemService itemService;
	
	@RequestMapping("/getheader")
	public Map<String, Object> getHeader() {
		Map<String, Object> model = new HashMap<String, Object>();
		model.put("id", UUID.randomUUID().toString());
		model.put("content", "Hello World! Yahoo!");
		return model;
	}

	@RequestMapping("/items")
	public List<Item> getItems() {
		return itemService.getItems();
	}
	
	@RequestMapping(value="/items", method=RequestMethod.POST)
	public Map<String, Object> postItem(@RequestBody Item item) {
		Map<String, Object> model = new HashMap<String, Object>();

		int maxId = itemService.addItem(item);
		model.put("maxId", maxId);

		return model;
	}
	
	@RequestMapping(value="/items/{id}", method=RequestMethod.PUT)
	public Map<String, Object> putItem(@RequestBody Item item) {
		Map<String, Object> model = new HashMap<String, Object>();

		Item updated = itemService.updateItem(item);
		model.put("updated", updated);

		return model;
	}
	
	@RequestMapping(value="/items", method=RequestMethod.DELETE)
	public Map<String, Object> deleteItem(@RequestHeader("Delete-Target") int id) {
		Map<String, Object> model = new HashMap<String, Object>();

		itemService.deleteItem(id);
		model.put("deletedId", id);

		return model;
	}
}
```

## ドメイン

RESTコントローラから登場したので、ここで記載。

```java:Item.java
package com.example.domain;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

/**
 * Created by saiki on 2016/09/01.
 */
@Data
public class Item {
    @Getter
    @Setter
    private int id;

    @Getter
    @Setter
    private String name;

    @Getter
    @Setter
    private String comment;

    @Getter
    @Setter
    private Date updateAt;
}
```
>Getter,Setterはlombokで記述しています。

## サービス

```java:ItemService.java
package com.example.service;

import com.example.domain.Item;
import com.example.repository.ItemRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.inject.Inject;
import java.util.List;

@Service
@Transactional
public class ItemService {

	@Inject
	private ItemRepository itemRepository;
	
	@Transactional(readOnly = true)
	public List<Item> getItems() {
		return itemRepository.getItems();
	}

	@Transactional
	public int addItem(Item item) {
		itemRepository.addItem(item);
		return itemRepository.getMaxId();
	}

	@Transactional
	public Item updateItem(Item item) {
		itemRepository.updateItem(item);
		return itemRepository.getItem(item.getId());
	}

	@Transactional
	public void deleteItem(int id) {
		itemRepository.deleteItem(id);
	}
	
}
```

## リポジトリ

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

import com.example.domain.Item;
import com.example.dto.DtoItem;
import com.example.mapper.ItemMapper;
import org.springframework.stereotype.Repository;

import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;

@Repository
public class ItemRepository {

    @Inject
    private ItemMapper itemMapper;

    public List<Item> getItems() {

        List<DtoItem> dto = itemMapper.list();
        List<Item> domain = new ArrayList<Item>();

        dto.stream().forEach(e -> {
            Item item = new Item();
            item.setId(e.getId());
            item.setName(e.getName());
            item.setComment(e.getComment());
            item.setUpdateAt(e.getUpdateAt());
            domain.add(item);
        });

        return domain;
    }

    public void addItem(Item item) {
        DtoItem dto = new DtoItem();
        dto.setName(item.getName());
        dto.setComment(item.getComment());
        itemMapper.add(dto);
    }

    public int getMaxId() {
        return itemMapper.getMaxId();
    }

    public void updateItem(Item item) {
        DtoItem dto = new DtoItem();
        dto.setId(item.getId());
        dto.setName(item.getName());
        dto.setComment(item.getComment());
        itemMapper.update(dto);
    }

    public Item getItem(int id) {
        DtoItem dto = itemMapper.getOne(id);
        Item domain = new Item();

        domain.setId(dto.getId());
        domain.setName(dto.getName());
        domain.setComment(dto.getComment());
        domain.setUpdateAt(dto.getUpdateAt());

        return domain;
    }

    public void deleteItem(int id) {
        itemMapper.delete(id);
    }
}
```

## DTO

リポジトリにたどり着いたので、DTOが出現。

```java:DtoItem.java
package com.example.dto;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

/**
 * Created by saiki on 2016/09/02.
 */
@Data
public class DtoItem {
    @Getter
    @Setter
    private int id;

    @Getter
    @Setter
    private String name;

    @Getter
    @Setter
    private String comment;

    @Getter
    @Setter
    private Date updateAt;
}
```

# データベースアクセス

ここからMyBatisのアノテーションが出てくるので、MyBatisの設定をします。

## MyBatisの設定

```java:
package com.example.config;


import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;

import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;

/**
 * Created by saiki on 2016/09/01.
 */
@Configuration
public class MyConfiguration {

    @Autowired
    @Bean
    public DataSourceTransactionManager transactionManager(
            @Qualifier("dataSource") DataSource dataSource) {

        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);

        return transactionManager;
    }

    @Autowired
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(
            @Qualifier("dataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);

        ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(
                new DefaultResourceLoader());

        // MyBatis のコンフィグレーションファイル
        bean.setConfigLocation(resolver.getResource("classpath:config/mybatis.xml"));
        // MyBatis で使用する SQL ファイル群
        bean.setMapperLocations(resolver.getResources("classpath:sql/*.xml"));

        return new SqlSessionTemplate(bean.getObject());
    }

    @Primary
    @Autowired
    @Bean
    public DriverManagerDataSource dataSource() {
        // 今回の例は ORACLE、適宜変更する
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(org.postgresql.Driver.class.getName());
        dataSource.setUrl("jdbc:postgresql://localhost:5432/mybatis");
        dataSource.setUsername("mybatisuser");
        dataSource.setPassword("mybatispass");

        return dataSource;
    }
}
```

```xml:mybatis.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- アンダースコア区切り (スネークケース) のカラム名をキャメルケースにマップする設定 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- SQL 内で AS によって設定された列名をマップする設定 -->
        <setting name="useColumnLabel" value="true"/>
    </settings>
</configuration>
```

>MyBatisの設定はここにありました
>http://www.mybatis.org/mybatis-3/ja/configuration.html

## マッパー

```java:ItemMapper.java
package com.example.mapper;

import com.example.dto.DtoItem;
import org.apache.ibatis.annotations.*;

import java.util.List;

/**
 * Created by saiki on 2016/09/01.
 */
@Mapper
public interface ItemMapper {
    List<DtoItem> list();
    void add(DtoItem dto);
    int getMaxId();
    void update(DtoItem dto);
    DtoItem getOne(int id);
    void delete(int id);
}
```

```xml:ItemMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.ItemMapper">
    <select id="list" resultType="com.example.dto.DtoItem">
        SELECT
        id,
        name,
        comment,
        update_at
        FROM item
        ORDER BY update_at DESC
    </select>
    <insert id="add" parameterType="com.example.dto.DtoItem">
        INSERT INTO item (
        name,
        comment
        )
        VALUES (
        #{name},
        #{comment}
        )
    </insert>
    <select id="getMaxId" resultType="int">
        SELECT
        max(id)
        FROM item
    </select>
    <update id="update" parameterType="com.example.dto.DtoItem">
        UPDATE item
        SET	name = #{name}, comment = #{comment}, update_at = now()
        WHERE id = #{id}
    </update>
    <select id="getOne" parameterType="int" resultType="com.example.dto.DtoItem">
        SELECT
        id,
        name,
        comment,
        update_at
        FROM item
        WHERE id = #{id}
    </select>
    <insert id="delete" parameterType="int">
        DELETE
        FROM item
        WHERE id=#{Id}
    </insert>
</mapper>
```

# よくわからんかったところ

- @Configration（Bean）でMyBatisの設定を行う（アノテーションだから適用タイミングがイメージできなかった）
- "マッパー"って広く使われすぎててググってもようわからん
- そもそもBean、DIに慣れてない

# 総評

もっとがんばりましょう