【Spring Boot入門】Eclipseで作るToDoアプリ:Spring基礎とORマッパーの実践

SpringBoot

完成品デモ

はじめに

SpringBootでToDoアプリを作ってみよう【誰でも作れます・初心者向け】 - Qiita
#はじめにSpringBootを使ってToDoアプリを作っていきます。一度動くものを作ってみるとイメージしやすいと思うので、これからSpringBootの学習を始める方、参考にしてみてください。…

こちらの記事を参考にして作成しております。すんなり作成できるかなと思っていたのですが、なかなか詰まったポイントもあったので、その点が差分となりますのでアウトプットします。

同じアプリを作成しましたが、使用しているDBやバージョンによって作り方を変更しないといけない点が多くその点勉強になりました。

開発環境

  • OS:Windows
  • DB:MySQL
  • Eclipse:Version: 2022-12
  • SpringBoot:3.3.4
  • ビルドツール:Gradle
  • Java17

SpringBootのアプリケーションを作成した事はあるが、ORマッパーを使用したアプリに関しては、業務で少し触った位であまり基礎から理解出来ていなかったため、作成しようと思いました。

ディレクトリの構成・ER図

DB作成/DDL・DML

SQL
-- todoアプリのデータベースを作成
CREATE DATABASE todo_db;

-- todoアプリのテーブルを作成

CREATE TABLE todo_items (
	id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
	title VARCHAR(50),
	done_flg INT DEFAULT 0,
	time_limit DATE NOT NULL 
);

-- テストデータを挿入
INSERT INTO todo_items (id, title, done_flg, time_limit) VALUES (1, 'Javaの勉強をする', 1, '2024-10-25');

作成手順

Springスタータープロジェクト作成

依存関係は以下で設定

プロジェクトが作成出来たら、DBViewerで作成したDBを紐づける

※すみません、こちら割愛します。

mybatisとspringを連携する

こちらを設定しないと以下のエラーではまります。

外部DTD: accessExternalDTDプロパティで設定された制限により’http’アクセスが許可されていないため、外部DTD ‘mybatis-mapper-3.0.dtd’の読取りに失敗しました。

src/main/java/com/todoの下に、「config」パッケージを作成します。

「config」を右クリックし、「新規(W)」→「クラス」と進みます。

MyBatisConfig.java
package com.example.todo.config;

import javax.sql.DataSource;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
// @org.springframework.transaction.annotation.EnableTransactionManagementを付与し、
// アノテーション駆動(@Transactional)のトランザクション制御を有効にします。
@EnableTransactionManagement
// @org.mybatis.spring.annotation.MapperScanを付与し、Mapperインターフェースのスキャンを有効にします。
@MapperScan("com.example.todo.mapper")
public class MyBatisConfig {

    // データソースのBean定義をします。
    @Bean
    public DataSource dataSource() {
        return DataSourceBuilder.create()
        		.url("jdbc:mysql://localhost:3306/DBの名前")
        		.username("設定した名前")
        		.password("設定したパスワード")
        		.build();
    }

    // org.mybatis.spring.SqlSessionFactoryBeanをBean定義します。
    // これによりSqlSessionFactoryBeanを利用してSqlSessionFactoryが生成されます。
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        // データソースを設定する。MyBatisの処理の中でSQLを発行すると、
        // ここで指定したデータソースからコネクションが取得されます。
        sessionFactoryBean.setDataSource(dataSource());
        // MyBatis設定ファイルを指定します。
        // 今回はresources直下に設定ファイルを配置します。
        sessionFactoryBean.setConfigLocation(new ClassPathResource("/myBatis-config.xml"));
        return sessionFactoryBean;
    }

    // トランザクションマネージャーのBeanを定義します。
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}

src/main/resourcesの下に「myBatis-config.xml」を作成します

myBatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="jdbcTypeForNull" value="NULL"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <typeAliases>
        <package name="com.example.todo"/>
    </typeAliases>
</configuration>
LocalDateの設定を行う

こちらを行わないと、DBでは日付の値を持っているにも関わらず、アプリケーション側ではnullとなるため、はまりました。

LocalDateTypeHandler .java
package com.example.todo;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

public class LocalDateTypeHandler extends BaseTypeHandler<LocalDate> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, LocalDate parameter, JdbcType jdbcType) throws SQLException {
        ps.setDate(i, java.sql.Date.valueOf(parameter));
    }

    @Override
    public LocalDate getNullableResult(ResultSet rs, String columnName) throws SQLException {
        java.sql.Date date = rs.getDate(columnName);
        return date != null ? date.toLocalDate() : null;
    }

    @Override
    public LocalDate getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        java.sql.Date date = rs.getDate(columnIndex);
        return date != null ? date.toLocalDate() : null;
    }

    @Override
    public LocalDate getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        java.sql.Date date = cs.getDate(columnIndex);
        return date != null ? date.toLocalDate() : null;
    }
}
myBatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="jdbcTypeForNull" value="NULL"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <typeAliases>
        <package name="com.example.todo"/>
    </typeAliases>
    <typeHandlers>
        <typeHandler handler="com.example.todo.LocalDateTypeHandler"         
         javaType="java.time.LocalDate"/>
    </typeHandlers>
</configuration>
Entityを作成する

src/main/java/com/todoの下に、「entity」パッケージを作成します。

app(com.todo)を右クリックし、「新規(W)」→「パッケージ」と進みます。

パッケージの下にTodo.javaクラスを作成します。

entityを右クリックし、「新規(W)」→「クラス」と進みます。

名前(M)に「Todo」と入力し、完了を押します。

Todo.java
package com.example.todo.entity;
import java.time.LocalDate;

import lombok.Data;

@Data
public class Todo {
	private Integer id;
	private String title;
	private Integer done_flg;
	private LocalDate time_limit;
}

※@Dataでsetter,getterを自動で作成している

Mapperを作成する

パッケージの下にTodoMapper.javaクラスを作成します。

mapperを右クリックし、「新規(W)→「インターフェース」と進みます。

名前(M)に「TodoMapper」と入力し、完了を押します。

TodoMapper.javaが作成されたので、編集していきます。

TodoMapper.java
package com.example.todo.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.example.todo.entity.Todo;
@Mapper
public interface TodoMapper {
 
public List<Todo> selectAll();
}
XMLファイルの作成

Mapperインターフェースと同じ構成のフォルダを作成し、XMLファイルを作成していきます。

src/main/resourcesを右クリックし、

「新規(W)」→「フォルダー」と進みます。

フォルダー名に、「com/todo/app/mapper」と入力し、完了します。

mapperフォルダを右クリックし、

「新規(W)」→「その他」→「XMLファイル」と進みます。

ファイル名は、「TodoMapper.xml」とします。

TodoMapper.xmlファイルが作成されたので、編集していきます。

TodoMapper.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.todo.mapper.TodoMapper">

  <!-- resultMapの定義 -->
  <resultMap id="todoResultMap" type="com.example.todo.entity.Todo">
      <result property="id" column="id"/>
      <result property="title" column="title"/>
      <result property="done_flg" column="done_flg" />
      <result property="time_limit" column="time_limit" 
      typeHandler="com.example.todo.LocalDateTypeHandler"/>
  </resultMap>

  <select id="selectAll" resultMap="todoResultMap">
      SELECT * FROM todo_items
  </select>
	
</mapper>
Thymeleafの作成

フロント部分を作っていきます。

src/main/resources/templatesの下に、index.htmlを作成します。

index.htmlが作成できたら、編集していきます。

index.html
<!DOCTYPE html>
<htmlxmlns:th="http://www.thymeleaf.org">
<head>
<metacharset="UTF-8">
<title>Todo</title>
</head>
<body>
  <h1>Todo-List</h1>
  <p th:each="todo : ${todos}"th:text="${todo.title}" />
</body>
</html>
Controllerの作成

src/main/java/com/todo/appの下に、「controller」パッケージを作成します。

controllerパッケージの下に、「TodoController.java」を作成します。

TodoController.javaが作成できたら、編集していきます。

TodoController .java
package com.example.todo.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import com.example.todo.entity.Todo;
import com.example.todo.mapper.TodoMapper;

@Controller
public class TodoController {
	
	@Autowired
	TodoMapper todoMapper;
	
	@GetMapping("/")
	public String index(Model model) {
		List<Todo> list = todoMapper.selectAll();

		model.addAttribute("todos", list);
		return "index";
	}

}
CRUD処理を実装する

ここからは、完成したコードを載せます。基礎的な知識があれば処理を追う事は可能だと思うので、是非追ってみてください。

TodoMapper.java
package com.example.todo.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.example.todo.entity.Todo;

@Mapper
public interface TodoMapper {
	
	public List<Todo> selectAll();
	
	public List<Todo> selectIncomplete();
	
	public List<Todo> selectComplete();
	
	public void add(Todo todo);
	
	public void update(Todo todo);
	
	public void delete();
}
TodoMapper.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.todo.mapper.TodoMapper">

	 <!-- resultMapの定義 -->
    <resultMap id="todoResultMap" type="com.example.todo.entity.Todo">
        <result property="id" column="id"/>
        <result property="title" column="title"/>
        <result property="done_flg" column="done_flg" />
        <result property="time_limit" column="time_limit" typeHandler="com.example.todo.LocalDateTypeHandler"/>
    </resultMap>

    <select id="selectAll" resultMap="todoResultMap">
        SELECT * FROM todo_items
    </select>
    
    <select id="selectIncomplete" resultMap="todoResultMap">
    	SELECT * FROM todo_items WHERE done_flg = 0
    </select>
    
    <select id="selectComplete" resultMap="todoResultMap">
    	SELECT * FROM todo_items WHERE done_flg = 1
    </select>
	
	<insert id="add" parameterType="com.example.todo.entity.Todo">
		INSERT INTO todo_items (title, time_limit)
		VALUES (#{title}, #{time_limit})
	</insert>
	
	<update id="update" parameterType="com.example.todo.entity.Todo">
		UPDATE todo_items 
		SET title = #{title}, time_limit = #{time_limit}, done_flg = #{done_flg}
		WHERE id = #{id}
	</update>
	
	<delete id="delete" parameterType="com.example.todo.entity.Todo">
		DELETE FROM todo_items WHERE done_flg = 1
	</delete>
	
</mapper>
TodoController.java
package com.example.todo.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.todo.entity.Todo;
import com.example.todo.mapper.TodoMapper;

@Controller
public class TodoController {
	
	@Autowired
	TodoMapper todoMapper;
	
	@GetMapping("/")
	public String index(Model model) {
//		List<Todo> list = todoMapper.selectAll();
		List<Todo> list = todoMapper.selectIncomplete();
		List<Todo> doneList = todoMapper.selectComplete();

		model.addAttribute("todos", list);
		model.addAttribute("doneTodos", doneList);
		return "index";
	}
	
	@PostMapping("/add")
	public String add(Todo todo) {
		todoMapper.add(todo);
		return "redirect:/";
	}
	
	@PostMapping("/update")
	public String update(Todo todo) {
		todoMapper.update(todo);
		return "redirect:/";
	}
	
	@PostMapping("/delete")
	public String delete() {
		todoMapper.delete();
		return "redirect:/";
	}
	
}
index.html
<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Todo</title>
</head>
<body>
	<h1>Todo-List</h1>
	<form method="post" th:action="@{/update}" th:each="todo : ${todos}">
		<input type="checkbox" name="done_flg" value="1" th:checked="${todo.done_flg == 1}" />
		<input type="hidden" name="done_flg" value="0" th:if="${todo.done_flg == 0}" />
		<input type="hidden" name="id" th:value="${todo.id}" />
		<input type="text" name="title" th:value="${todo.title}" />
		<input type="date" name="time_limit" th:value="${todo.time_limit}" />
		<input type="submit" value="更新" />
	</form>
	
	
	<h3>完了済み</h3>
	<form method="post" th:action="@{/update}" th:each="todo : ${doneTodos}">
		<input type="checkbox" name="done_flg" value="1" />
		<input type="hidden" name="id" th:value="${todo.id}" />
		<input type="text" name="title" th:value="${todo.title}" style="text-decoration:line-through"/>
		<input type="date" name="time_limit" th:value="${todo.time_limit}" />
		<input type="submit" value="更新" />
	</form>

	
	<h3>新しいタスクを追加</h3>
	<form method="post" th:action="@{/add}">
		<input type="text" name="title" />
		<input type="date" name="time_limit" />
		<input type="submit" value="追加" />	
	</form>
	
	<form method="post" th:action="@{/delete}">
		<input type="submit" value="完了済みを削除" />
	</form>
	
</body>
</html>

おわりに

説明をあまり書いてませんが、キータの記事が丁寧に書いてあるので、参考いただければと思います。このアプリを土台として、改良して勉強したいと考えています。

お読みいただきありがとうございました。

コメント