JHipsterの自動生成JPAをMyBatis化

2017年のjsugで@s_kozakeさんが紹介していたJHipsterを今更触ってみました。

そのときの@s_kozakeさん資料
[実践JHipster]
(https://speakerdeck.com/kozake/shi-jian-jhipster-number-jsug-number-sf-36)

JHipsterにせっかくエンティティサブジェネレータがあったり、
JPAは海外で圧倒的なシェアがあったり、
Spring Data JPAのJPA RepositoryのCRUDがあったりで
素直にJPA使った方がいいのでしょうが、
周りにJPA苦手な人（自分含め）が多いのでMyBatisで代替してみます。

\#読み終わるとJPA覚えた方がいいのでは？という感想になる気がします。

#バージョン
JHipster v6.1.2
すでにだいぶ古くなってしまった。。

#実装

ソースは以下
https://github.com/ko-aoki/jhreact

## type of application 
Monolithic applicationを選択しています。
記事中には出てこないけどなんとなくReact.jsを選択。
JavaScriptな人たちはやっぱりReact.js好きなんだろなーという思いが。


## pom.xml
まずpom.xmlにmybatis-spring-boot-starterのdependencyを追加。
@kazuki43zooさんのこちらです。
[mybatis-spring-boot-starterの使い方](https://qiita.com/kazuki43zoo/items/ea79e206d7c2e990e478)

```pom.xml
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>2.1.0</version>
        </dependency>
```

ちょっとそれますが、datasourceがどこから取得されているか確認してみました。
[MybatisAutoConfiguration.java](https://github.com/mybatis/spring-boot-starter/blob/master/mybatis-spring-boot-autoconfigure/src/main/java/org/mybatis/spring/boot/autoconfigure/MybatisAutoConfiguration.java)の
@ConditionalOnSingleCandidate(DataSource.class)アノテーションがあって



```MybatisAutoConfiguration.java
// (抜粋)
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {

```

[DataSourceConfiguration.java](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java)に
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource"アノテーションがあって

```DataSourceConfiguration.java
// (抜粋)
	/**
	 * Hikari DataSource configuration.
	 */
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(HikariDataSource.class)
	@ConditionalOnMissingBean(DataSource.class)
	@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
			matchIfMissing = true)
	static class Hikari {

		@Bean
		@ConfigurationProperties(prefix = "spring.datasource.hikari")
		HikariDataSource dataSource(DataSourceProperties properties) {
			HikariDataSource dataSource = createDataSource(properties, 			HikariDataSource.class);
			if (StringUtils.hasText(properties.getName())) {
				dataSource.setPoolName(properties.getName());
			}
			return dataSource;
		}

	}
```

application-dev.xmlにdatasourceがあると。

```application-dev.xml
# (抜粋)
  datasource:
    type: com.zaxxer.hikari.HikariDataSource

```

##自動生成JPAの移行

以下のようにdomainパッケージ配下のEntityクラスから
JPAアノテーションを削除してMyBatisのresultMap-Typeとして使用します。

```Authority.java
// (抜粋)
// @Entity
// @Table(name = "jhi_authority")
public class Authority implements Serializable {
    @NotNull
    @Size(max = 50)
//    @Id
//    @Column(length = 50)
    private String name;


```

自動生成されていたRepositoryに対応するMapperを作成します。

```AuthorityDomainMapper.java
package com.example.domain.mapper;

import com.example.domain.Authority;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Optional;

@Mapper
public interface AuthorityDomainMapper {

    Optional<Authority> findById(@Param("name") String name);

    List<Authority> findAll();

    void save(@Param("authority") Authority authority);

    void delete(@Param("authority") Authority authority);
}
```

```AuthorityDomainMapper.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.domain.mapper.AuthorityDomainMapper">
    <resultMap id="authority" type="com.example.domain.Authority">
        <id property="name" column="name"/>
    </resultMap>
    <select id="findById" resultMap="authority">
        select
            name
        from jhi_authority
        where
            name = #{name}
    </select>
    <select id="findAll" resultMap="authority">
        select
            name
        from jhi_authority
    </select>
    <insert id="create" parameterType="com.example.domain.Authority">
          insert into
              jhi_authority
          (
            name
          )
          values
          (
            #{name}
          )
    </insert>
    <delete id="delete" parameterType="com.example.domain.Authority">
        delete
        from
            jhi_authority
        where
            name = #{name}
    </delete>
</mapper>
```

Mapperを呼び出すRepositoryを作成します。


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

import com.example.domain.Authority;
import com.example.domain.mapper.AuthorityDomainMapper;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public class AuthorityRepository {

    AuthorityDomainMapper mapper;


    public AuthorityRepository(AuthorityDomainMapper mapper) {
        this.mapper = mapper;
    }

    public Optional<Authority> findById(String name) {
        return this.mapper.findById(name);
    }

    public List<Authority> findAll() {
        return this.mapper.findAll();
    }
}

```

こんな記事書いてなんですが、上記が自動生成だと↓だけだから、
これだけでもSpring Data JPAいいなという気もしますよね。

```JpaRepository.java
// (自動生成)
package com.example.repository;

import com.example.domain.Authority;

import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Spring Data JPA repository for the {@link Authority} entity.
 */
public interface AuthorityRepository extends JpaRepository<Authority, String> {
}

```

JPAのJOINしている箇所はもう少し面倒です。
こちらが自動生成部分。

```User.java
// 自動生成
// 抜粋
@Entity
@Table(name = "jhi_user")
public class User extends AbstractAuditingEntity implements Serializable {

// 中略

    @JsonIgnore
    @ManyToMany
    @JoinTable(
        name = "jhi_user_authority",
        joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
        inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")})

    @BatchSize(size = 20)
    private Set<Authority> authorities = new HashSet<>();

// 後略

```
MyBatisなのでSQLに直したのが以下。
面倒だけどSQLの一目瞭然さから離れられない。。

```UserDomainMapper.xml
<!-- 前略 -->
<mapper namespace="com.example.domain.mapper.UserDomainMapper">
    <resultMap id="user" type="com.example.domain.User">
<!-- 中略 -->
        <collection property="authorities" ofType="com.example.domain.Authority">
            <id property="name" column="authority_name"/>
        </collection>
    </resultMap>
<!-- 中略 -->
    <select id="findOneWithAuthoritiesById" resultMap="user">
        select
        u.id as id,
<!-- 中略 -->
        a.name as authority_name
        from jhi_user u
        inner join
        jhi_user_authority ua
        on
        u.id = ua.user_id
        inner join
        jhi_authority a
        on
        ua.authority_name = a.name
        where
            u.id = #{id}
    </select>
<!-- 後略 -->

```

JPAと同等の表現は難しいと感じたのが以下の自動生成ソース。
MapみたいなEntity以外の1:nってMyBatisでは難しい。

```PersistentAuditEvent.java
// 自動生成
// 前略
@Entity
@Table(name = "jhi_persistent_audit_event")
public class PersistentAuditEvent implements Serializable {

// 中略

    @ElementCollection
    @MapKeyColumn(name = "name")
    @Column(name = "value")
    @CollectionTable(name = "jhi_persistent_audit_evt_data", joinColumns=@JoinColumn(name="event_id"))
    private Map<String, String> data = new HashMap<>();

// 後略

```
対象のテーブルはこちらです。
(liquibaseのChangeLog抜粋)

```00000000000000_initial_schema.xml
        <createTable tableName="jhi_persistent_audit_event">
            <column name="event_id" type="bigint" autoIncrement="true">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="principal" type="varchar(50)">
                <constraints nullable="false" />
            </column>
            <column name="event_date" type="timestamp"/>
            <column name="event_type" type="varchar(255)"/>
        </createTable>
        <createTable tableName="jhi_persistent_audit_evt_data">
            <column name="event_id" type="bigint">
                <constraints nullable="false"/>
            </column>
            <column name="name" type="varchar(150)">
                <constraints nullable="false"/>
            </column>
            <column name="value" type="varchar(255)"/>
        </createTable>
```
MyBatisの環境で
無理にMapにする必要もないけど、無理やり対応してみました。

KeyValueのBeanを作成して

```KeyValue.java
public class KeyValue {

    private String key;
    private String value;
// 後略

}
```
ネストされたselectでKeyValueを取得して

```PersistenceAuditEventDomainMapper.xml
    <resultMap id="persistentAuditEvent" type="com.example.domain.PersistentAuditEvent">
        <id property="eventId" column="event_id"/>
        <id property="principal" column="principal"/>
        <id property="auditEventDate" column="event_date"/>
        <id property="auditEventType" column="event_type"/>
        <collection property="keyValueList" column="event_id" ofType="KeyValue" select="findAuditEventDataById"/>
    </resultMap>
    <select id="findAuditEventDataById" resultMap="keyValue">
        select
            name key,
            value value
        from
            jhi_persistent_audit_evt_data
        where
            event_id = #{eventId}
    </select>

```

EntityでMapに変換。
なんてことになりました。

```PersistentAuditEvent.java
// 前略
public class PersistentAuditEvent implements Serializable {
// 中略
    private List<KeyValue> keyValueList = new ArrayList<>();

    public Map<String, String> getData() {
        if (keyValueList == null) {
            return Collections.EMPTY_MAP;
        }

        return keyValueList.stream().collect(
            Collectors.toMap(KeyValue::getKey, KeyValue::getValue)
        );
    }

    public void setKeyValueList(List<KeyValue> data ) {
        this.keyValueList = data;
    }

    public void setData(Map<String, String> dataMap ) {

        if (dataMap == null) {
            return;
        }

        List<KeyValue> dataList = dataMap.entrySet().stream()
            .map(
                d -> {
                    KeyValue kv = new KeyValue();
                    kv.setKey(d.getKey());
                    kv.setValue(d.getValue());
                    return kv;
                }
            )
            .collect(
                Collectors.toList()
            );
        this.setKeyValueList(dataList);
    }
// 後略

```
#まとめ

JPAは敬遠されるから。。と軽い動機でやり始めてみましたが
かなり手数が必要な作業になりました。
JPAの表現力を軽視して敬遠し続けるのもよくないですね！