Javaでよく聞くContoroller、Service、Repository(、Dao、DTO)、Entity。
オブジェクト指向では必須の知識です。
今回はその中でもRepository(リポジトリー)について
サンプルコードを用いて解説していきます!
サンプルコードの説明については、
コード内のコメントに記載しています。
また、今回のコードは、以下のGitHubにも載せています。
このサイトで見にくい場合は、GitHubをご覧ください。
Repository(リポジトリー)とは?
データベースの操作をするオブジェクトを保存するクラスです。
Repositoryと非常に似たものに、Dao(Database Access Object)というものがありますが、
この2つの違いは様々な解釈がありそうですので、あくまでも僕の解釈で話します。
この2つの違いは、関心の向き先です。
Repositoryは、オブジェクトに関心をむけ、データベース操作を直感的にわかりやすくする目的であり、
Daoは、データベース操作をするということのみに関心が向いている、
といった違いです。
(プロジェクトによっては、RepositoryとDaoを同じように扱うプロジェクトもあるようですので、
そこまで深く考える必要もないかもですね)
とりあえず、Repositoryは、
というように理解しておきましょう。
さらに、メソッドは直感的に何をするのかを理解できるメソッド名にする必要もあります。
Repositoryで扱うのは、CRUD操作(Create, Read, Update, Delete)です。
一般的にメソッド名としては、find, save, delete が使用されているイメージです。
ごちゃごちゃ言うよりはサンプルコードを見た方が分かりやすいかと思いますので、
以下をご覧ください!
なお、コードの説明はコード内のコメントに記載しています。
環境・定義
build.gradle
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.2.5'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.hibernate:hibernate-core:5.4.24.Final'
implementation 'javax.persistence:javax.persistence-api:2.2'
implementation 'org.springframework.data:spring-data-commons:3.2.0'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
//コンソールに取得データや発行したSQLを表示するために使用
runtimeOnly 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4:1.16'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//JUnit
testImplementation 'org.junit.jupiter:junit-jupiter:5.5.2'
}
tasks.named('test') {
useJUnitPlatform()
}
application.properties
spring.application.name=RepositorySample
#DBの接続方法やコンソールに発行したSQLや取得したデータを表示するための設定
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
#MySQLの設定
spring.datasource.url=jdbc:log4jdbc:mysql://localhost/ENTITY_TEST
spring.datasource.username=root
spring.datasource.password=パスワード(独自で設定)
CREATE_DATABASE_ENTITY_TEST.sql
CREATE DATABASE ENTITY_TEST;
CREATE_TABLE_ENTITY_TEST_TABLE
CREATE TABLE ENTITY_TEST_TABLE
(id INT NOT NULL
,full_name VARCHAR(255) NULL
,insert_date INT NULL
,PRIMARY KEY (id));
INSERT_DATA.sql
INSERT INTO ENTITY_TEST_TABLE (id, full_name ,insert_date) VALUES (1, 'エンティティ ダンプティ', 20240418);
INSERT INTO ENTITY_TEST_TABLE (id, full_name ,insert_date) VALUES (2, 'エンティティ ティティ', 20240418);
INSERT INTO ENTITY_TEST_TABLE (id, full_name ,insert_date) VALUES (3, 'エンティティ えん', 20240418);
INSERT INTO ENTITY_TEST_TABLE (id, full_name ,insert_date) VALUES (4, 'エンティティ ダ ヴィンチ', 20240418);
ファイル階層
- /
- src/
- main/
- java/
- com/example/demo/
- entity/
- Entity.java
- rowmapper/
- EntityRowMapper.java
- Repository/
- AbstractRepository.java
- IRepository.java
- extend/
- Repository.java
- entity/
- com/example/demo/
- resources/
- application.properties
- CREATE_DATABASE_ENTITY_TEST.sql
- CREATE_TABLE_ENTITY_TEST.sql
- INSERT_DATA.sql
- java/
- test/
- java/com/example/demo/repository/
- TestRepository.java
- java/com/example/demo/repository/
- main/
- build.gradle
- ……
- src/
サンプルコード
Entity.java
package com.example.demo.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Entity {
private Integer id;
private String full_name;
private Integer insert_date;
public static String TABLE_NAME = "ENTITY_TEST_TABLE";
public static String ID = "id";
public static String FULL_NAME = "full_name";
public static String INSERT_DATE = "insert_date";
}
EntityRowMapper.java
package com.example.demo.entity.rowmapper;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.RowMapper;
import com.example.demo.entity.Entity;
public class EntityRowMapper implements RowMapper<Entity> {
@Override
public Entity mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Entity(
rs.getInt(Entity.ID),
rs.getString(Entity.FULL_NAME),
rs.getInt(Entity.INSERT_DATE)
);
}
}
Entity(エンティティ)については、他の記事にまとめています。
こちらもぜひご覧ください。
IRepository.java
package com.example.demo.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.dao.DataAccessException;
/**
* <pre>
* リポジトリクラスのインターフェース
* 実装の際はエンティティクラスを渡す
* </pre>
* @author Takumi
*
* @param <T> エンティティクラス
*/
public interface IRepository<T> {
/**
* 指定したidのデータを取得する
* @param id
* @return 指定したidのデータ
* @throws DataAccessException
*/
Optional<T> findById(Object id) throws DataAccessException;
/**
* データをすべて取得する
* @return テーブルデータすべて
* @throws DataAccessException
*/
List<T> findAll() throws DataAccessException;
/**
* 指定したキーのidを削除する
* @param id
* @throws DataAccessException
*/
void deleteById(Object id) throws DataAccessException;
/**
* @param saveList 保存したいデータのリスト
* 複数データを保存する
* @throws Exception
* @throws Exception
*/
void save(List<T> saveList) throws Exception;
/**
* @param saveData 保存したいデータ
* データを1つ保存する
* @throws Exception
*/
void save(T saveData) throws Exception;
}
AbstractRepository.java
package com.example.demo.repository;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import lombok.RequiredArgsConstructor;
/**
* <pre>
* リポジトリの抽象クラス
* コンストラクタには{@code <S>}のオブジェクトを生成したクラスを引数に渡す
* </pre>
* @author Takumi
* @param <T> エンティティクラス
* @param <S> {@link RowMapper}を実装したクラス
*/
@RequiredArgsConstructor
public abstract class AbstractRepository<T, S> implements IRepository<T> {
@Autowired
protected NamedParameterJdbcTemplate namedjdbcTemplate;
/** {@link #findAll()}や{@link #findById(Object)}で取得したデータを格納 */
private List<T> dataList;
/** <pre>
* {@link RowMapper}を実装したクラスを型に持つ
* {@link #findAll()}や{@link #findById(Object)}のマッピングで使用
* </pre> */
protected final S rowMapper;
/** {@link #findAll()}や{@link #findById(Object)}で取得したデータ数 */
@SuppressWarnings("unused")
private int findCount;
/** {@link #save(List)}や{@link #save(Object)}で保存したデータ数 */
private int saveCount;
/** {@link #deleteById(Object)}で削除したデータ数 */
private int deleteCount;
public int getFindCount() {
return dataList.size();
}
public int getSaveCount() {
return saveCount;
}
public void setSaveCount(int saveCount) {
this.saveCount = saveCount;
}
public int getDeleteCount() {
return deleteCount;
}
public void setDeleteCount(int deleteCount) {
this.deleteCount = deleteCount;
}
public List<T> getDataList() {
return dataList;
}
public void setDataList(List<T> dataList) {
this.dataList = dataList;
}
}
Repository.java
package com.example.demo.repository.extend;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.namedparam.EmptySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import com.example.demo.entity.Entity;
import com.example.demo.entity.rowmapper.EntityRowMapper;
import com.example.demo.repository.AbstractRepository;
/**
* <pre>
* リポジトリクラス
* {@code @Autowired}で使用
* DIコンテナに登録するため必ず{@link org.springframework.stereotype.Repository}を付与
* </pre>
* @author Takumi
*
*/
@org.springframework.stereotype.Repository
public class Repository extends AbstractRepository<Entity, EntityRowMapper> {
public Repository() {
super(new EntityRowMapper());
}
@Override
public List<Entity> findAll() throws DataAccessException {
List<String> select = new ArrayList<>();
select.add("SELECT");
select.add("*");
select.add("FROM");
select.add(Entity.TABLE_NAME);
try {
setDataList(namedjdbcTemplate.query(String.join(" ", select), new EmptySqlParameterSource(), rowMapper));
return getDataList();
} catch (DataAccessException e) {
System.err.println(e.getMessage() + "\r\n" + e.getStackTrace());
throw e;
}
}
@Override
public Optional<Entity> findById(Object id) throws DataAccessException {
List<String> select = new ArrayList<>();
select.add("SELECT");
select.add("*");
select.add("FROM");
select.add(Entity.TABLE_NAME);
select.add("WHERE");
select.add(Entity.ID + " = :" + Entity.ID);
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue(Entity.ID, id);
try {
setDataList(namedjdbcTemplate.query(String.join(" ", select), params, rowMapper));
if (getFindCount() == 0) {
return Optional.ofNullable(null);
} else {
//データは1つであることが前提
return Optional.ofNullable(getDataList().get(0));
}
} catch (DataAccessException e) {
System.err.println(e.getMessage() + "\r\n" + e.getStackTrace());
throw e;
}
}
@Override
public void deleteById(Object id) throws DataAccessException {
List<String> delete = new ArrayList<>();
delete.add("DELETE");
delete.add("FROM");
delete.add(Entity.TABLE_NAME);
delete.add("WHERE");
delete.add(Entity.ID + " = :" + Entity.ID);
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue(Entity.ID, id);
try {
setDeleteCount(namedjdbcTemplate.update(String.join(" ", delete), params));
} catch (DataAccessException e) {
System.err.println(e.getMessage() + "\r\n" + e.getStackTrace());
throw e;
}
}
@Override
public void save(List<Entity> saveList) throws Exception {
SqlParameterSource[] batchParam = SqlParameterSourceUtils.createBatch(saveList);
List<String> save = new ArrayList<>();
try {
save.add("INSERT INTO");
save.add(Entity.TABLE_NAME);
setInsertSqlByMap(save);
save.add("ON DUPLICATE KEY");
save.add("UPDATE");
setDuplicateUpdateSqlByMap(save);
save.add(";");
int[] saveCounts = namedjdbcTemplate.batchUpdate(String.join(" ", save), batchParam);
int saveCount = 0;
for (int count : saveCounts) {
saveCount += count;
}
setSaveCount(saveCount);
} catch (Exception e) {
System.err.println(e.getMessage() + "\r\n" + e.getStackTrace());
e.printStackTrace();
throw e;
}
}
@Override
public void save(Entity saveData) throws Exception {
Map<String, Object> fieldNameAndValue = getFieldNameAndValue(saveData);
MapSqlParameterSource params = new MapSqlParameterSource();
try {
List<String> save = new ArrayList<>();
//データがなければインサート、あればアップデートをかける
save.add("INSERT INTO");
save.add(Entity.TABLE_NAME);
setInsertSqlByMap(save, fieldNameAndValue, params);
save.add("ON DUPLICATE KEY");
save.add("UPDATE");
setDuplicateUpdateSqlByMap(save, fieldNameAndValue);
save.add(";");
String sql = String.join(" ", save);
setSaveCount(namedjdbcTemplate.update(sql, params));
} catch (Exception e) {
System.err.println(e.getMessage() + "\r\n" + e.getStackTrace());
throw e;
}
}
/**
* エンティティのstaticフィールド以外のフィールド名とその値を{@code Map<String, Object>}として返す
* @param entity
* @return key:フィールド名 value:そのフィールドの値 の{@code Map<String, Object>}
* @throws Exception
*/
private Map<String, Object> getFieldNameAndValue(Entity entity) throws Exception {
try {
Map<String, Object> map = new LinkedHashMap<>();
Field[] fields = entity.getClass().getDeclaredFields();
for (Field field : fields) {
//privateフィールドへのアクセスを可能にする
field.setAccessible(true);
//staticフィールドの場合はスキップする
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
String name = field.getName();
Object value = field.get(entity);
map.put(name, value);
}
return map;
} catch (IllegalArgumentException | IllegalAccessException e) {
System.err.println(e.getMessage() + "\r\n" + e.getStackTrace());
throw e;
}
}
/**
* エンティティのフィールド名を{@code List<String>}にして返す
* @return フィールド名をつめた{@code List<String>}
* @throws Exception
*/
private List<String> getFieldNames() throws Exception {
try {
List<String> list = new ArrayList<>();
Field[] fields = new Entity().getClass().getDeclaredFields();
for (Field field : fields) {
//privateフィールドへのアクセスを可能にする
field.setAccessible(true);
// staticフィールドの場合はスキップする
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
list.add(field.getName());
}
return list;
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage() + "\r\n" + e.getStackTrace());
throw e;
}
}
/**
* <pre>
* key名 = VALUES(:key名), key名 = VALUES(:key名), .....
* を作成する
* </pre>
* @param sqlList 作成中のSQL文
* @param fieldNameAndValue key:カラム名 value:更新したい値
*/
private void setDuplicateUpdateSqlByMap(List<String> sqlList, Map<String, Object> fieldNameAndValue) {
List<String> updateSqlList = new ArrayList<>();
for (var entrySet : fieldNameAndValue.entrySet()) {
updateSqlList.add(entrySet.getKey() + " = VALUES(" + entrySet.getKey() + ")");
}
sqlList.add(String.join(", ", updateSqlList));
}
private void setDuplicateUpdateSqlByMap(List<String> sqlList) throws Exception {
List<String> updateSqlList = new ArrayList<>();
try {
for (String fieldName : getFieldNames()) {
updateSqlList.add(fieldName + " = VALUES(" + fieldName + ")");
}
} catch (Exception e) {
e.printStackTrace();
throw e;
}
sqlList.add(String.join(", ", updateSqlList));
}
/**
* <pre>
* (key名, key名, key名, ...) VALUES (:key名, :key名, .....)
* を作成する
* </pre>
* @param sqlList 作成中のSQL文
* @param fieldNameAndValue key:カラム名 value:その値 のMap<String, Object> {@link #getFieldNameAndValue(Entity)}を使用して作成
* @param params 作成中のパラメター
*/
private void setInsertSqlByMap(List<String> sqlList, Map<String, Object> fieldNameAndValue,
MapSqlParameterSource params) {
String[] columnArray = new String[fieldNameAndValue.size()];
String[] valueArray = new String[fieldNameAndValue.size()];
int index = 0;
for (var entrySet : fieldNameAndValue.entrySet()) {
String setKey = entrySet.getKey();
int idx = 0;
while (params.hasValue(setKey)) {
setKey = setKey + Integer.valueOf(idx).toString();
idx++;
}
params.addValue(setKey, entrySet.getValue());
columnArray[index] = entrySet.getKey();
valueArray[index] = ":" + setKey;
index++;
}
sqlList.add("( " + String.join(", ", columnArray) + " ) VALUES ( " + String.join(", ", valueArray) + " )");
}
/**
* <pre>
* (key名, key名, key名, ...) VALUES (:key名, :key名, .....)
* を作成する
* </pre>
* @param sqlList 作成中のSQL文
* @throws Exception
*/
private void setInsertSqlByMap(List<String> sqlList) throws Exception {
List<String> fieldNames;
try {
fieldNames = getFieldNames();
} catch (Exception e) {
e.printStackTrace();
throw e;
}
String[] columnArray = new String[fieldNames.size()];
String[] valueArray = new String[fieldNames.size()];
int index = 0;
for (String fieldName : fieldNames) {
columnArray[index] = fieldName;
valueArray[index] = ":" + fieldName;
index++;
}
sqlList.add("( " + String.join(", ", columnArray) + " ) VALUES ( " + String.join(", ", valueArray) + " )");
}
}
NamedParameterJdbcTemplateの使用方法については、他の記事へまとめています。
こちらもぜひご覧ください。
TestRepository.java (Repository使用例)
package com.example.demo.repository;
import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.entity.Entity;
import com.example.demo.repository.extend.Repository;
@SpringBootTest
@Transactional
class TestRepository {
@Autowired
private Repository repository;
@Test
void testFindAll() {
List<Entity> dataList = repository.findAll();
assertEquals(repository.getFindCount(), 4);
//本来は全データチェックするべきだが、ここではidが2のデータのみ確認
Entity data_Id2 = dataList.stream().filter(data -> Integer.valueOf(data.getId()) == 2).toList().get(0);
assertEquals(data_Id2.getFull_name(), "エンティティ ティティ");
assertEquals(data_Id2.getInsert_date(), 20240418);
}
@Test
void testFindById() {
Optional<Entity> dataOpt = repository.findById(1);
if (dataOpt.isPresent()) {
Entity data = dataOpt.get();
assertEquals(repository.getFindCount(), 1);
assertEquals(data.getFull_name(), "エンティティ ダンプティ");
assertEquals(data.getInsert_date(), 20240418);
} else {
assertEquals(repository.getFindCount(), 0);
}
}
@Test
void testDeleteById() {
repository.deleteById(1);
assertEquals(repository.getDeleteCount(), 1);
Optional<Entity> dataId1Opt = repository.findById(1);
//データがないことで削除確認
assertTrue(dataId1Opt.isEmpty());
}
@Test
void testSave() throws Exception {
repository.save(new Entity(2, "テスト ダンプティ", 20240507));
repository.save(new Entity(8, "テスト", 20240506));
Optional<Entity> data_Id8Opt = repository.findById(8);
Entity data_Id8 = data_Id8Opt.isPresent() ? data_Id8Opt.get() : null;
Optional<Entity> data_Id2Opt = repository.findById(2);
Entity data_Id2 = data_Id2Opt.isPresent() ? data_Id2Opt.get() : null;
//データがなければエラー
if (data_Id2 == null || data_Id8 == null) {
fail("バグっとる");
}
assertEquals(data_Id2.getFull_name(), "テスト ダンプティ");
assertEquals(data_Id2.getInsert_date(), 20240507);
assertEquals(data_Id8.getFull_name(), "テスト");
assertEquals(data_Id8.getInsert_date(), 20240506);
}
@Test
void testBatchSave() throws Exception {
List<Entity> saveList = new ArrayList<>();
Entity entity1 = new Entity(2, "テスト ダンプティ", 20240507);
Entity entity2 = new Entity(8, "テスト", 20240506);
saveList.add(entity1);
saveList.add(entity2);
repository.save(saveList);
Optional<Entity> data_Id8Opt = repository.findById(8);
Entity data_Id8 = data_Id8Opt.isPresent() ? data_Id8Opt.get() : null;
Optional<Entity> data_Id2Opt = repository.findById(2);
Entity data_Id2 = data_Id2Opt.isPresent() ? data_Id2Opt.get() : null;
if (data_Id2 == null || data_Id8 == null) {
fail("バグっとる");
}
assertEquals(data_Id2.getFull_name(), "テスト ダンプティ");
assertEquals(data_Id2.getInsert_date(), 20240507);
assertEquals(data_Id8.getFull_name(), "テスト");
assertEquals(data_Id8.getInsert_date(), 20240506);
}
}
このように、RepositoryをAutowiredするだけで、
直感的に何をしているのかが分かります。(findAllはどう考えても全データ抽出してますよね)
Repositoryはデータベース操作のオブジェクトを保存し、
直感的に扱うことが可能です。
参考サイト