今回のお題
今回は、私が感じたSpring Bootと他の言語との違いについて書きます。
メインテーマはORMですね。
タイトルにある通り、私はJava自体の初学者ですが、同じようにJavaの学習を始めたばかりの方、あるいは今後自分が他の言語の学習を始める際の参考になればという思いで残しておきます。
筆者の学習歴
プログラミング学習歴は4ヶ月程度。
スクールではRuby on Railsを、独学ではPHP Laravelを学習し、それぞれアプリらしきものの作成経験はあり。
Javaについては1ヶ月ほど前から学習を開始。
現在は書籍を見ながらSpring Bootアプリの作成手順を学習中。
本記事のターゲット
冒頭でも少し触れましたが、本記事は主に「既に学習済みの言語とSpring Bootの違いに戸惑っている方」向けに作成しました。
書籍やネットなどで学習をしていて、「なぜこのようなやり方をしなければならないのかが分からない。」、「他の(自分の知っている)言語と同じような手順でやってはダメなのか。」と言った疑問が湧くことがあるかと思いますが、それらの解決になれば幸いです。
逆に、Javaからプログラミングの学習を始めたという方にとってはあまり参考にならないかもしれません。
あまり「なぜ?」と考えすぎずに「そういうものなんだ」と割り切って受け入れるぐらいの方がうまくいくと思います。
ORMとは
ORM(Object Relational Mapping)とは、データベースのレコードをオブジェクト指向言語のオブジェクトに変換する技術のことです。
代表例はRuby on RailsのActiveRecordやPHP LaravelのEloquentですね。
例えば、Railsでよく見る以下のコード。
@user = User.find(1)
サラッと1行で書いていますが、実際には以下の3つの作業が行われています。
- 上記のコードが以下のSQLに変換されて実行され、レコードが検索される。
- 検索したレコードを元に、Userモデルのインスタンスが生成される。
- 生成されたインスタンスが@userという変数に代入される。
select * from users where id=1
そして、レコードとインスタンスを相互に変換できるということは、両者が同じような設計図になっていないと困りますよね。
なので、Railsではテーブルとモデルの間に以下のルールを設けています(全てUserモデルを例に説明。クラス名などは適宜読み替えてください)。
- rails g model userコマンドを実行すると、Userモデルとそれに対応するusersテーブル及びマイグレーションファイルも自動的に生成される。
- usersテーブルにカラムが作成されると、Userモデルも自動的に同じ名前のインスタンス変数を持つようになる。
- Userモデルに対してActiveRecordメソッドを用いると、対応するusersテーブルを対象に自動的にSQLが実行される。allやfindなどのレコード取得のSQLを実行した場合には取得したレコードを元にしたインスタンスが作成され、これがActiveRecordメソッドの戻り値になる。
つまり、テーブルがあればそれに合わせたモデルも半ば自動的に作成されるようになっているということですね。
もし、上記の長い説明が分かりにくければ、これだけを頭に入れた上で次に進んでください。
Spring BootにはORMの機能はない
そして、ここからが本題なのですが、Spring BootにはORMの機能がありません。
ということは、RailsやLaravelでは勝手に実行されていた
- ある関数が呼び出された際に、どのようなSQLが実行されるのか
- あるテーブルのレコードをどのモデルのインスタンスに変換するのか
- テーブル内のどのカラムを変換先モデルのどのインスタンスフィールドと対応させるのか
などを全て自分で決めなければなりません。
例えば、
@user = User.find(params[:id])
に相当するコードはjavaでは以下になるのですが(importやアノテーションなどは適宜省略しています)、
public String getUserOne(UserDetailForm form, Model model, @PathVariable("userId")String userId){
MUser user = service.getUser(userId);
UserDetailForm form = mapper.(user, UserDetailForm.class);
model.addAttribute("userDeatilForm", form);
return "user/detail";
}
このgetUserメソッドの中身やテーブルとモデル、カラムとフィールドの対応関係を自分で決めないといけないので、以下が追加で必要になります。
package com.example.demo.application.service;
import java.util.List;
import com.example.demo.domain.user.model.MUser;
public interface UserService {
public MUser getUserOne(String userId);
}
package com.example.demo.domain.user.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.application.service.UserService;
import com.example.demo.domain.user.model.MUser;
import com.example.demo.repository.UserMapper;
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper mapper;
@Override
public MUser getUserOne(String userId) {
return mapper.findOne(userId);
}
}
<?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.demo.repository.UserMapper">
<resultMap type="com.example.demo.domain.user.model.MUser" id="user">
<id column="user_id" property="userId"/>
<result column="password" property="password"/>
<result column="user_name" property="userName"/>
<result column="birthday" property="birthday"/>
<result column="age" property="age"/>
<result column="gender" property="gender"/>
<!-- <result column="department_id" property="departmentId"/> -->
<result column="role" property="role"/>
<!-- 追加 -->
<association property="department" resultMap="department"/>
<!-- 追加 -->
<collection property="salaryList" resultMap="salary" columnPrefix="salary_"/>
</resultMap>
<!-- 追加 -->
<resultMap type="com.example.demo.domain.user.model.Department" id="department">
<id column="department_id" property="departmentId"/>
<result column="department_name" property="departmentName"/>
</resultMap>
<!-- 追加 -->
<resultMap id="salary" type="com.example.demo.domain.user.model.Salary">
<id column="user_id" property="userId"/>
<id column="year_month" property="yearMonth"/>
<result column="salary" property="salary"/>
</resultMap>
<insert id="insertOne">
insert into m_user(user_id, password, user_name, birthday, age, gender, department_id, role)values(#{userId}, #{password}, #{userName}, #{birthday}, #{age}, #{gender}, #{departmentId}, #{role})
</insert>
<select id="findMany" resultType="MUser">
select * from m_user
<where>
<if test="userId != null">
user_id like '%' || #{userId} || '%'
</if>
<if test="userName != null">
and user_name like '%' || #{userName} || '%'
</if>
</where>
</select>
<select id="findOne" resultMap="user">
<!-- 表示するカラムを*から変更 -->
select
m_user.user_id
, m_user.password
, m_user.user_name
, m_user.birthday
, m_user.age
, m_user.gender
, m_department.department_id
, m_department.department_name
, t_salary.user_id as salary_user_id
, t_salary.year_month as salary_year_month
, t_salary.salary as salary_salary
from m_user
left join m_department on m_user.department_id = m_department.department_id
<!-- 追加 -->
left join t_salary on m_user.user_id = t_salary.user_id
<!-- 修正 -->
where m_user.user_id = #{userId}
</select>
<update id="updateOne">
update m_user set password = #{password}, user_name = #{userName} where user_id = #{userId}
</update>
<delete id="deleteOne">
delete from m_user where user_id = #{userId}
</delete>
</mapper>
package com.example.demo.repository;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.example.demo.domain.user.model.MUser;
@Mapper
public interface UserMapper {
public MUser findOne(String userId);
}
基本的にテーブル対モデル・カラム対フィールドの対応関係を記述しているのはxmlファイルです。
また、このファイルには実際に行われるSQLも記述されています。
そのほかのファイルについては、mapper, インターフェース, サービスと記述を分けるという都合上ファイルが多くなっているという感じです。
何はともあれ、JavaのコードとSQLのコードの通訳を開発者側でやってあげないといけないというのが、Javaの手間なところですね。
終わりに
以上で本記事は終了です。
やや冗長な説明になってしまった感もありますが、今回お伝えしたかったことは以下の2点に集約されます。
- 他の言語では自動で処理されていることでも、Javaでは自分で定義しないといけないことが多い。
- その例が、本記事で紹介したSQL文への翻訳や、テーブル対モデル、カラム対フィールドの対応関係の設定。
他にも@Autowieredやフォームなど色々と躓いた点はあるので、そこについてもいずれまとめます。