2017年のjsugで@s_kozakeさんが紹介していたJHipsterを今更触ってみました。
そのときの@s_kozakeさん資料
実践JHipster
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の使い方
<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の
@ConditionalOnSingleCandidate(DataSource.class)アノテーションがあって
// (抜粋)
@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に
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource"アノテーションがあって
// (抜粋)
/**
* 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があると。
# (抜粋)
datasource:
type: com.zaxxer.hikari.HikariDataSource
自動生成JPAの移行
以下のようにdomainパッケージ配下のEntityクラスから
JPAアノテーションを削除してMyBatisのresultMap-Typeとして使用します。
// (抜粋)
// @Entity
// @Table(name = "jhi_authority")
public class Authority implements Serializable {
@NotNull
@Size(max = 50)
// @Id
// @Column(length = 50)
private String name;
自動生成されていたRepositoryに対応するMapperを作成します。
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);
}
<?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を作成します。
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いいなという気もしますよね。
// (自動生成)
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している箇所はもう少し面倒です。
こちらが自動生成部分。
// 自動生成
// 抜粋
@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の一目瞭然さから離れられない。。
<!-- 前略 -->
<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では難しい。
// 自動生成
// 前略
@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抜粋)
<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を作成して
public class KeyValue {
private String key;
private String value;
// 後略
}
ネストされたselectでKeyValueを取得して
<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に変換。
なんてことになりました。
// 前略
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の表現力を軽視して敬遠し続けるのもよくないですね!