Controller, Service, Repository(, Dao, Dto), Entityと
javaにはオブジェクト指向という考え方があります。
今回はその中でも今回は、
Serviceクラスについて、サンプルコードを交えて解説していきます。
Serviceとは
簡潔に言うと、実際の処理を記述する場所です。
ただ、その処理の中でもDB(データベース)を実際に扱うところは、
RepositoryやDaoクラスに任せるので、その他の部分をServiceクラスでは扱います。
Serviceクラスを作成していく際に、注意が必要なのは、
処理ごとにメソッドを分けるということです。
このようなことを言っても分かりにくいかと思いますので、
実際にサンプルコードを見てみましょう。
以下で紹介するサンプルは、すべてGitHubへ載せています。
こちらも参照ください。
環境・定義
build.gradle
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.3.0'
id 'io.spring.dependency-management' version '1.1.5'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
//コンソールに取得データや発行した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=ServiceSample
#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=パスワード(独自で設定)
#springDataJPA設定
spring.jpa.hibernate.ddl-auto=validate
データベースは、ローカルのmysqlを使用します。
schema(データベースやテーブル), data(データ)の
SQLのサンプルについては、後ほど示します。
データベースの作成方法に関しては、
Entity記事のデータベース作成以下を参照ください。
また、spring.jpa.hibernate.ddl-autoの設定値については、
Spring Data JPAのapplication.propertiesを参照ください。
CREATE_DATABASE_ENTITY_TEST.sql
CREATE DATABASE ENTITY_TEST;
CREATE_TABLE_ENTITY_TEST_TABLE.sql
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
- form/
- Form.java
- repository/
- Repository.java
- service/
- extend/
- Service.java
- AbstractService.java
- IService.java
- extend/
- ServiceSampleApplication.java
- ServletInitializer.java
- entity/
- com/example/demo/
- resourrces/
- application.properties
- CREATE_DATABASE_ENTITY_TEST.sql
- CREATE_TABLE_ENTITY_TEST_TABLE.sql
- INSERT_DATA.sql
- java/
- test/
- java/
- com/example/demo/
- service/extend/
- TestService.java
- …
- service/extend/
- com/example/demo/
- java/
- main/
- build.gradle
- setttings.gradle
- …
- src/
サンプルコード
Entity.java
package com.example.demo.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* ENTITY_TEST__TABLEテーブルのEntity
* @author Takumi
*/
@jakarta.persistence.Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ENTITY_TEST_TABLE")
public class Entity {
@Id
private Integer id;
@Column(name = "full_name")
private String fullName;
private Integer insert_date;
}
このクラスはデータベースのテーブルデータ1つ1つを表します。
Entityについては以下の記事をご覧ください。
Form.java
package com.example.demo.form;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import io.micrometer.common.util.StringUtils;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* <pre>
* 画面より入力されることを想定したクラス
* 型チェックも行う
* </pre>
* @author Takumi
*/
@Data
public class Form {
@NotEmpty
@Pattern(regexp = "\\d", message = "idは数値のみ入力可能です")
private String id;
@NotEmpty
private String full_name;
@Pattern(regexp = "\\d", message = "insert_dateは数値のみ入力可能です")
private String insert_date;
/**
* 値がセットされていない場合は、
* 現在日時をyyyyMMdd形式で取得
* @return yyyyMMdd
*/
public String getInsert_date() {
if(!StringUtils.isBlank(this.insert_date)) {
return this.insert_date;
}
LocalDateTime nowDate = LocalDateTime.now();
DateTimeFormatter yyyyMMdd_formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
String yyyyMMdd = yyyyMMdd_formatter.format(nowDate);
return yyyyMMdd;
}
}
今回は実際の画面を作っていないですが、
実際には、画面のinputタグから入力されたデータを格納するクラスです。
このクラスでアノテーションを用いることでバリデーションをすることもできます。
(ここで使用している@Patternもその一つです)
もう少し詳細にバリデーションしたい場合は、メソッドを作成するのも1つの手です。
また、@Patternにあるmessaageですが、message.propertiesクラスにセットしておき、
{ }にkey名を入れることで呼び出すこともできます。
Repository.java
package com.example.demo.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.entity.Entity;
/**
* Spring Data JPAを使用
* @author Takumi
*/
public interface Repository extends JpaRepository<Entity,Integer>{
public List<Entity> findByFullName(String fullName);
}
Spring Data JPAを使用します。
この詳細については以下の記事をご覧ください。
Serviceクラス関連
ここからが本題です。
今回は、Serviceクラスのインターフェースと抽象クラスを作成しています。
インターフェースは、処理の共通の概要を示し、
抽象クラスは、処理で使用する変数を格納するために使用しています。
IService.java
package com.example.demo.service;
import java.util.List;
import com.example.demo.entity.Entity;
import com.example.demo.form.Form;
/**
* サービスクラスのインターフェース
* @author Takumi
*/
public interface IService {
/**
* データをすべて取得
*/
void findAll();
/**
* 対象のIdのデータを取得
*/
Entity findById(int userId);
/**
* ユーザー名からデータを取得
* @param userName ユーザー名
*/
void findByUserName(String userName);
/**
* ユーザーを1件追加(INSERT)
* @param form
*/
void addUser(Form form);
/**
* ユーザーを追加
* @param formList 追加するユーザー
*/
void addUser(List<Form> formList);
/**
* ユーザーを削除
* @param userId 削除するユーザーのID
*/
void deleteUser(int userId);
/**
* データを1件編集(UPDATE)
* @param form 更新するユーザーのデータ
*/
void editUser(Form form);
/**
* データを編集(UPDATE)
* @param formList 編集するデータ
*/
void editUser(List<Form> formList);
}
このように、処理を記載した、仕様書のようなものをインターフェースに作成しておきます。
必ずコメントを入れておきましょう。
インターフェースを使用することで、処理の抜けを防ぐことができたり、
開発者やコードを読む人にクラスの概要をわかりやすくしたりするメリットがあります。
そのままクラスを作成し、Serviceを作成する開発者も多々いますが、
私はインターフェースを作成してからの方が、処理がわかりやすくなるかと思います。
AbstractService.java
package com.example.demo.service;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* 継承先でリポジトリクラスはセットする。
* @author Takumi
*/
@Data
public abstract class AbstractService<T> implements IService {
/** 取得したデータ */
protected List<T> dataList = new ArrayList<T>();
/** 取得したデータ数 */
protected int dataListSize;
/** 追加したデータ数 */
protected int addCount;
/** 削除したデータ数 */
protected int deleteCount;
/** 編集したデータ数 */
protected int editCount;
public int getDataListSize() {
return dataList.size();
}
}
このクラスは、処理で使用するフィールドを格納するために使用しています。
今回はありませんが、継承先で定義したいフィールドがある場合には、
修飾子にabstractをつけると、継承先で定義するフィールドとして認識されます。
またこのクラスは、先ほどのIService.javaを継承していることで、
実際に使用するServiceクラスは、このAbstractServiceクラスを継承することで
作成できるようにしています。
Service.java
package com.example.demo.service.extend;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.demo.entity.Entity;
import com.example.demo.form.Form;
import com.example.demo.repository.Repository;
import com.example.demo.service.AbstractService;
/**
* @author Takumi
*/
@org.springframework.stereotype.Service
public class Service extends AbstractService<Entity> {
@Autowired
private Repository repository;
/** 処理対象のデータを格納 */
private List<Entity> entityList;
@Override
public void findAll() {
List<Entity> list = repository.findAll();
setDataList(list);
}
@Override
public Entity findById(int userId) {
Optional<Entity> dataOpt = repository.findById(Integer.valueOf(userId));
//データがない場合はnullを返す
return dataOpt.isPresent() ? dataOpt.get() : null;
}
@Override
public void findByUserName(String userName) {
List<Entity> list = repository.findByFullName(userName);
setDataList(list);
}
@Override
public void addUser(Form form) {
//データの存在確認をする
if (isExist(form)) {
System.err.println("idが" + form.getId() + "のデータはすでに存在します。");
} else {
save(form);
setAddCount(getAddCount() + 1);
}
}
@Override
public void addUser(List<Form> formList) {
List<Form> existList = existList(formList);
//データの存在確認をする
if (existList.size() == 0) {
List<Entity> savedList = saveAll(formList);
setAddCount(getAddCount() + savedList.size());
} else {
isExistDatasErrorLog(existList, true);
}
}
@Override
public void deleteUser(int userId) {
//データの存在確認をする
if (isExist(userId)) {
Integer id = Integer.valueOf(userId);
repository.deleteById(id);
setDeleteCount(getDeleteCount() + 1);
} else {
System.err.println("idが" + userId + "のデータは存在しません。");
}
}
@Override
public void editUser(Form form) {
//データの存在確認をする
if (isExist(form)) {
save(form);
setEditCount(getEditCount() + 1);
} else {
System.err.println("idが" + form.getId() + "のデータは存在しません。");
}
}
@Override
public void editUser(List<Form> formList) {
//データの存在確認をする
if (existList(formList).size() == 0) {
isExistDatasErrorLog(formList, false);
} else {
List<Entity> savedList = saveAll(formList);
setEditCount(getEditCount() + savedList.size());
}
}
/**
* 存在する複数データのログを出力
* @param existList 存在する複数データ
* @param isExist 存在するログを出力する場合はtrue
*/
private void isExistDatasErrorLog(List<Form> existList, boolean isExist) {
String[] existId = new String[existList.size()];
for (int i = 0; i < existList.size(); i++) {
existId[i] = existList.get(i).getId();
}
if (isExist) {
System.err.println("idが" + String.join(", ", existId) + "のデータはすでに存在します。");
} else {
System.err.println("idが" + String.join(", ", existId) + "のデータは存在しません。");
}
}
/**
* データの存在確認をする
* @param form 存在確認をするデータ
* @return 存在すればtrue
*/
private boolean isExist(Form form) {
Integer ID = Integer.parseInt(form.getId());
int id = ID.intValue();
return findById(id) != null;
}
/**
* データの存在確認をする
* @param userId 存在確認をするデータのid
* @return 存在すればtrue
*/
private boolean isExist(int userId) {
return findById(userId) != null;
}
/**
* 存在するデータのみを返す
* @param form 存在確認するデータの{@code List<Form}
* @return 存在したデータの{@code List<Form}
*/
private List<Form> existList(List<Form> form) {
List<Form> existList = new ArrayList<>();
for (Form data : form) {
Integer ID = Integer.parseInt(data.getId());
int id = ID.intValue();
Entity d = findById(id);
if (d != null) {
existList.add(data);
}
}
return existList;
}
/**
* {@link Form}クラスを{@link Entity}クラスへ変換する
* @param form
* @return 変換後の{@link Entity}クラス
*/
private Entity setEntity(Form form) {
Entity entity = new Entity();
entity.setId(Integer.parseInt(form.getId()));
entity.setFullName(form.getFull_name());
entity.setInsert_date(Integer.parseInt(form.getInsert_date()));
return entity;
}
/**
* INSERTもしくはUPDATEを実行
* @param form 更新、挿入したいデータ
* @return 更新、挿入したデータの{@link Entity}クラス
*/
private Entity save(Form form) {
Entity entity = setEntity(form);
return repository.save(entity);
}
/**
* 複数のINSERTもしくはUPDATEを実施
* @param formList 更新、挿入したいデータ
* @return 更新、挿入したいデータ
*/
private List<Entity> saveAll(List<Form> formList) {
setEntityListByFromList(formList);
return repository.saveAll(entityList);
}
/**
* {@link this#entityList}を{@link Form}クラスからセットする
* @param formList
*/
private void setEntityListByFromList(List<Form> formList) {
List<Entity> entityList = new ArrayList<>();
for (Form form : formList) {
entityList.add(setEntity(form));
}
setEntityList(entityList);
}
public List<Entity> getEntityList() {
return entityList;
}
public void setEntityList(List<Entity> entityList) {
this.entityList = entityList;
}
}
実際に使用するサービスクラスです。
抽象クラスである、AbstractService.javaを継承することで、
このクラスが継承していたIService.javaインターフェースを実装しています。
実装の際に、処理を分けたり、共通処理をまとめるための
メソッドも追加しています。
このようにServiceクラスは、実処理を行い、
そこで得られたデータをコントローラに引き継ぎ、画面表示するなどを
します。
サービスクラスを作成するにあたって大切なことは、
処理を細かくメソッドに分ける
ということです。
そうすることで、可読性も向上し、修正もしやすいコードとなります。
また、javadocコメントも必ず書いておきましょう。
このようにServiveクラスでは、実際の処理を記述します。
その中でもデータベースを扱うところはRepositoryに任せ、最終的に
Controllerに情報を渡してアプリケーションを作ります。
このように作成することで、可読性がまし、
非常に修正しやすいコードになります。
参考サイト