JHipsterでMyBatisを使ってみた
みなさんこんにちは、@ultaroです!
JPA(Hibernate)は、その強力なオブジェクト・リレーショナルマッピング(ORM)機能と、DBを隠ぺい化し、SQLを直接書かずとも、Javaのメソッド呼び出しでデータベース操作を行えるため多くの開発者に愛されています。オブジェクト指向のプログラミング言語とリレーショナルデータベースの間で生じるインピーダンスミスマッチを解決し、開発者はオブジェクト指向のモデルに集中することで、データベースアクセスの複雑さを意識することなく開発を進めることができます。
しかし、時にはその抽象化が原因で、パフォーマンスの問題や特定のクエリが柔軟に変更できないことが問題になることもありますよね?
そんなとき、手軽にSQLを直接書けるMyBatisが便利です。
私たちがよく担当するエンタープライズ向けのお客様でも直接SQLを書けるMyBatisは人気です。
今回は、JHipsterのORMツールとして、標準のJPA(Hibernate)ではなく、MyBatisを使えるか検証してみたいと思います。
JHipter公式ではサポートしていないんですが、どうにかやったる!
ということでぜひ、最後までお付き合いください!
前提条件
今回の検証では以下のバージョンを利用しています。
Java 17.0.10
JHipster 8.3.0
1.JHipsterプロジェクトの作成
jhipsterコマンドを実行し、次の通り質問に回答してJHipsterプロジェクトを作成します。 今回、回答の必要が無いものはエンターキーを押してスキップしています。
また?What is your default Java package name?への回答は本来は”com.mycompany.myapp”のようなパッケージ名になります。本記事ではわかりやすくするために[my_package_name]と記載しています。
? What is the base name of your application? --> mybatisapp
? Which *type* of application would you like to create? --> Monolithic application (recommended for simple projects)
? Besides Junit, which testing frameworks would you like to use? --> (skip)
? Do you want to make it reactive with Spring WebFlux? --> No
? Which *type* of authentication would you like to use? --> JWT authentication (stateless, with a token)
? Which *type* of database would you like to use? --> SQL (H2, PostgreSQL, MySQL, MariaDB, Oracle, MSSQL)
? Which *production* database would you like to use? --> PostgreSQL
? Which *development* database would you like to use? --> PostgreSQL
? Which cache do you want to use? (Spring cache abstraction) --> Ehcache (local cache, for a single node)
? Do you want to use Hibernate 2nd level cache? --> No
? Which other technologies would you like to use? --> (skip)
? Which *Framework* would you like to use for the client? --> React
? Besides Jest/Vitest, which testing frameworks would you like to use? --> (skip)
? Do you want to generate the admin UI? --> Yes
? Would you like to use a Bootswatch theme (https://bootswatch.com/)? --> Default JHipster
? What is your default Java package name? --> [my_package_name]
? Would you like to use Maven or Gradle for building the backend? --> maven
? Would you like to enable internationalization support? --> No
? Please choose the native language of the application English --> (skip)
2.下準備
liquibaseの設定
JHipsterではDB管理にliquibaseが使われています。
liquibaseのDBスキーマの差分管理機能を使うために、以下のコマンドを実行します。
jhipster --incremental-changelog
次にliquibaseの設定を変更します。
開発環境用の設定ファイル、src\main\resources\config\application-dev.ymlにデフォルトで設定されているfakeデータは扱いが面倒なので今回は使わないようにしておきます。
(+部分が今回追加分。-部分は今回削除分。他は自動生成されたコードです)
liquibase:
# Remove 'faker' if you do not want the sample data to be loaded automatically
+ contexts: dev
- contexts: dev, faker
Entityの準備
以下のコマンドでEntityを作成することができます。
jhipster entity <Entity名>
今回は例としてBook Entityを作成します。
jhipster entity Book
コマンド実行するといくつかの質問をされますので、以下のように回答していきます。Bookのフィールドには、StringのtitleとIntegerのpriceが存在していることにします。
? Do you want to add a field to your entity? --> Yes
? What is the name of your field? --> title
? What is the type of your field? --> String
? Do you want to add validation rules to your field? --> Yes
? Which validation rules do you want to add? --> Required, Maximum length
? What is the maximum length of your field? --> 50
? Do you want to add a field to your entity? --> Yes
? What is the name of your field? --> price
? What is the type of your field? --> Integer
? Do you want to add validation rules to your field? --> Yes
? Which validation rules do you want to add? --> Required, Minimum, Maximum
? What is the minimum of your field? --> 0
? What is the maximum of your field? --> 30000
? Do you want to add a field to your entity? --> No
? Do you want to add a relationship to another entity? --> No
? Do you want to use separate service class for your business logic? --> No, the REST controller
should use the repository directly
? Is this entity read-only? --> No
? Do you want pagination and sorting on your entity? --> No
全部答えると、.jhipster\Book.jsonに以下のようなEntity定義が出力されます。
<Book.json>
{
"annotations": {
"changelogDate": "20240425074900"
},
"fields": [
{
"fieldName": "title",
"fieldType": "String",
"fieldValidateRules": ["required", "maxlength"],
"fieldValidateRulesMaxlength": "50"
},
{
"fieldName": "price",
"fieldType": "Integer",
"fieldValidateRules": ["required", "min", "max"],
"fieldValidateRulesMax": "30000",
"fieldValidateRulesMin": 0
}
],
"incrementalChangelog": true,
"name": "Book",
"pagination": "no",
"readOnly": false,
"relationships": [],
"searchEngine": "no",
"service": "no"
}
また、自動生成でBook.javaクラスなども出力されます。
<Book.java>
package [my_package_name].domain;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.io.Serializable;
/**
* A Book.
*/
@Entity
@Table(name = "book")
@SuppressWarnings("common-java:DuplicatedBlocks")
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
@SequenceGenerator(name = "sequenceGenerator")
@Column(name = "id")
private Long id;
@NotNull
@Size(max = 50)
@Column(name = "title", length = 50, nullable = false)
private String title;
@NotNull
@Min(value = 0)
@Max(value = 30000)
@Column(name = "price", nullable = false)
private Integer price;
以下略
}
Book.javaの中にはidという自動採番のフィールドが存在します。
しかし、このクラスにアノテーションで設定されたシーケンス定義はJPA用の定義で、この後の工程で削除してしまうため使えません。
このため、idの自動採番はDBに直接設定する必要があります。
id自動採番用に以下のようなチェンジログを作成し、src\main\resources\config\liquibase\master.xmlにパスを追記します。
<作成するチェンジログ>
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="20240425074901-1" author="ulsystems">
<addAutoIncrement tableName="book" columnName="id" />
</changeSet>
</databaseChangeLog>
今回は20240425074901_added_autoincrement_book_id.xmlという名前でチェンジログを作成しました。
<master.xml>
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
(省略)
<include file="config/liquibase/changelog/00000000000000_initial_schema.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20240425074900_added_entity_Book.xml" relativeToChangelogFile="false"/>
+ <include file="config/liquibase/changelog/20240425074901_added_autoincrement_book_id.xml" relativeToChangelogFile="false"/>
<!-- jhipster-needle-liquibase-add-changelog - JHipster will add liquibase changelogs here -->
<!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here -->
<!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here -->
</databaseChangeLog>
3.MyBatisプラグインの追加
いろいろ準備がありましたが、ここからが本題です。
pom.xmlのdependenciesにMyBatisの依存関係を追加します。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
4.Entityの置き換え
JPAで生成されたEntityをMyBatis用に置き換えます。
これはJPA系のアノテーションを削除するだけでOKです。 JPA系のimport文を削除することで、IDEやエディタが警告文を出してくれるので、 指摘されたアノテーションを削除していきます。
package [my_package_name].domain;
-import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.io.Serializable;
/**
* A Book.
*/
-@Entity
-@Table(name = "book")
@SuppressWarnings("common-java:DuplicatedBlocks")
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
- @Id
- @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
- @SequenceGenerator(name = "sequenceGenerator")
- @Column(name = "id")
private Long id;
@NotNull
@Size(max = 50)
- @Column(name = "title", length = 50, nullable = false)
private String title;
@NotNull
@Min(value = 0)
@Max(value = 30000)
- @Column(name = "price", nullable = false)
private Integer price;
// jhipster-needle-entity-add-field - JHipster will add fields here
(省略)
5. Repositoryの実装(part1)
MyBatisを使用するためのRepositoryを実装します。
JHipsterで自動生成されたRepositoryは、JpaRepositoryを継承しているので、その記述を削除します。
さらにJPAのパッケージインポート文を削除し、関連するアノテーションを削除します。(IDE/エディタが警告を出してくれると思います。)
また自動生成ではRepositoryはインターフェースでしたが、クラスに変更します。※今回は簡易的な検証のためインターフェースをクラスに変更しますが、インターフェースのまま、別に実装クラスを作成してもOKです。
該当Repository名で検索して(もしくはIDE/エディタのエラー欄から)、使用されているメソッドを実装します。 この時点ではまだMapperが作成されていないので、シグネチャを定義するだけにしておきます。
つまり・・・
<削除するもの>
JPAのインポート文
JPAのアノテーション
JpaRepositoryの継承
<追加・変更するもの>
Repositoryをインターフェースからクラスへ変更
クラスに変更したことで実装が必要なメソッドを実装
package [my_package_name].repository;
import [my_package_name].domain.Book;
-import org.springframework.data.jpa.repository.*;
+
+import jakarta.validation.Valid;
+
+import java.util.Optional;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
@SuppressWarnings("unused")
@Repository
-public interface BookRepository extends JpaRepository<Book, Long> {}
+public class BookRepository {
+
+ public @Valid Book save(@Valid Book book) {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'save'");
+ }
+
+ public boolean existsById(Long id) {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'existsById'");
+ }
+
+ public Optional<Book> findById(Long id) {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'findById'");
+ }
+
+ public List<Book> findAll() {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'findAll'");
+ }
+
+ public void deleteById(Long id) {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'deleteById'");
+ }
+
+ public long count() {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'count'");
+ }
+
+}
6. mapperインターフェースとXMLの作成
mapperインターフェースを作成します。
基本はrepositoryで定義したシグネチャに合うようにメソッドを定義します。 しかしMyBatisで対応していない型(Optionalなど)もあるので、使える型で返すようにします。 saveメソッドはinsertとupdateの双方に対応している点などにも気を付けましょう。
<mapperインターフェース>
package [my_package_name].domain.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import [my_package_name].domain.Book;
@Mapper
public interface BookMapper {
void insert(Book book);
void update(Book book);
boolean existsById(Long id);
Book findById(Long id);
List<Book> findAll();
void deleteById(Long id);
long count();
}
<mapper XML>
XMLファイルはsrc/main/resources/[my_package_name]/domain/mapper以下に配置します。
insertメソッドでは、自動採番を利用するためにuseGeneratedKeys="true"としています。
<?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="[my_package_name].domain.mapper.BookMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id" parameterType="[my_package_name].domain.Book">
insert into book (title, price)
values ( #{title}, #{price} )
</insert>
<update id="update">
update book set title = #{title}, price = #{price}
where id = #{id}
</update>
<select id="existsById" resultType="_boolean">
select count(*) from book
where id = #{id}
</select>
<select id="findById" resultType="[my_package_name].domain.Book">
select * from book
where id= #{id}
</select>
<select id="findAll" resultType="[my_package_name].domain.Book">
select * from book
order by id
</select>
<delete id="deleteById">
delete from book
where id=#{id}
</delete>
<select id="count" resultType="_long">
select count(*) from book
</select>
</mapper>
7. Reposiotryの実装(part2)
6で定義したmapperを利用して、各メソッドの中身を実装していきます。 OptionalやPageableなどmybatisで対応していない部分を埋めるようにします。
package [my_package_name].repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import [my_package_name].domain.Book;
import [my_package_name].domain.mapper.BookMapper;
import jakarta.validation.Valid;
/**
* Repository for the Book entity.
*/
@SuppressWarnings("unused")
@Repository
public class BookRepository {
BookMapper mapper;
public BookRepository(BookMapper mapper) {
this.mapper = mapper;
}
public @Valid Book save(@Valid Book book) {
if (book.getId() == null) {
mapper.insert(book);
} else {
mapper.update(book);
}
return book;
}
public boolean existsById(Long id) {
return mapper.existsById(id);
}
public Optional<Book> findById(Long id) {
return Optional.ofNullable(mapper.findById(id));
}
public List<Book> findAll() {
return mapper.findAll();
}
public void deleteById(Long id) {
mapper.deleteById(id);
}
public long count() {
return mapper.count();
}
}
8.動作確認
ではブラウザから確認してみましょう。 今回は開発用DBにPostgreSQLを選択したので、dockerを立ち上げておきます。
docker compose -f src/main/docker/postgresql.yml up -d
アプリケーションを起動します。このときliquibaseのチェンジログがDBに反映されます。
./mvnw
http://localhost:8080/ でトップページにアクセスできます。
ログインした後に、EntitiesタブからBookにアクセスすると、CRUD操作ができることが確認できました!
まとめ
ちょっと修正は必要ですが、JHipsterでMyBatisを使うことができましたね!
💡でも…最後に注意点があります💡
属性が追加された時などに、EntityやRepositoryを再度自動生成する場合は、Repositoryクラスを上書きしないようにしましょう!
(せっかく書き換えた変更がなくなってしまいます!)
再生成時にRepositoryを上書きするかどうかを問われたら、do not overwriteを選んでください。
? Overwrite src/main/java/[my_package_name]/repository/BookRepository.java? (ynarxdeiH)
y) overwrite
n) do not overwrite
(省略)
Entityは追加フィールド分のgetterなどを追加するためにoverrideするのもアリですが、JPA系アノテーションも追加されてしまうので、その部分は手動で削除しましょう。
実装や運用で注意点はありますが、JHipsterでMyBatisを使うことができました!JHipsterでMyBatisを利用したい場合は、ぜひ参考にしてみてください!