LoginSignup
3
3

More than 3 years have passed since last update.

JHipsterの自動生成JPAをMyBatis化

Posted at

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の使い方

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
@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
@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の表現力を軽視して敬遠し続けるのもよくないですね!

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3