サービス・リポジトリ・エンティティとは何か
バックエンド設計の基本用語「サービス(ビジネスロジック)」「リポジトリ」「エンティティ」を、図を使ってゼロから理解する
この記事の目標
バックエンドのコードを読んでいると、Service・Repository・Entity という言葉が頻繁に登場する。
これらは「何のために存在するのか」「なぜ分けるのか」を、コード例と図で一から理解する。
全体像:コードの"層"で役割を分ける
バックエンドAPIは、外から来たリクエストに対して「データを調べ、必要な処理をして、結果を返す」という流れで動く。
このとき、処理を役割ごとの層(レイヤー)に分けて書くのが一般的な設計スタイルだ。
graph TD
Client["クライアント(ブラウザ / アプリ)"]
Controller["Controller層<br/>(リクエストの受け口)"]
Service["Service層<br/>(ビジネスロジック)"]
Repository["Repository層<br/>(DBアクセス)"]
DB[("データベース")]
Client -->|HTTPリクエスト| Controller
Controller -->|処理を依頼| Service
Service -->|データを要求・保存| Repository
Repository -->|SQL実行| DB
DB -->|結果| Repository
Repository -->|データを返す| Service
Service -->|結果を返す| Controller
Controller -->|HTTPレスポンス| Client
各層の責任はシンプルだ。
| 層 | 主な責任 |
|---|---|
| Controller | HTTPリクエストを受け取り、Serviceに処理を渡す。レスポンスを返す |
| Service | ビジネスロジックを実装する。Repositoryを使ってDBにアクセスする |
| Repository | DBへのアクセス(SQL実行)だけを担当する |
| Entity | DBのテーブル構造をコードで表現したもの |
エンティティ(Entity)とは
エンティティ = データベースのテーブルをクラスで表現したもの
たとえば users テーブルがあったとする。
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);このテーブルに対応するエンティティは、TypeScriptであれば次のように書く。
// user.entity.ts
export class User {
id: number;
name: string;
email: string;
createdAt: Date;
}エンティティはいわば「テーブルの設計図をコードで写したもの」だ。
DBから取得したデータを入れる器として使い、アプリ内でデータを運ぶ役割を担う。
erDiagram
users {
int id PK
string name
string email
timestamp created_at
}
↑ DBのテーブル定義と、エンティティクラスは1対1に対応するリポジトリ(Repository)とは
リポジトリ = 特定のテーブル(またはSQLクエリ)に対するDB操作をまとめたクラス
「DBからデータを取る・保存する・更新する・削除する」という操作を、ひとつのクラスに集約する。
Service側がSQLを直接書く必要がなくなり、「何を取得するか」ではなく「取得したデータをどう使うか」に集中できる。
具体例
// user.repository.ts
export class UserRepository {
// IDでユーザーを1件取得する
async findById(id: number): Promise<User | null> {
// ここにSQLやORMの操作を書く
return db.query(`SELECT * FROM users WHERE id = $1`, [id]);
}
// 全ユーザーを取得する
async findAll(): Promise<User[]> {
return db.query(`SELECT * FROM users`);
}
// ユーザーを新規作成する
async create(name: string, email: string): Promise<User> {
return db.query(
`INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *`,
[name, email]
);
}
}Repository は「DBとの窓口係」のようなイメージだ。
Service が「ユーザーID:5番のデータをくれ」と言えば、Repository が実際にSQLを実行して返してくれる。
リポジトリの単位
リポジトリは テーブル単位 または データ取得SQL単位 で作成する。
graph LR
UserRepo["UserRepository<br/>users テーブル担当"]
OrderRepo["OrderRepository<br/>orders テーブル担当"]
UserOrderRepo["UserOrderRepository<br/>ユーザーと注文を JOIN するクエリ担当"]
DB[("DB")]
UserRepo --> DB
OrderRepo --> DB
UserOrderRepo --> DB
テーブルが増えたり、特殊な集計SQLが増えても、それぞれ独立したRepositoryで管理できるため、変更の影響範囲が限定される。
サービス(Service)とは
サービス = ビジネスロジックを実装するクラス
「ビジネスロジック」という言葉が分かりにくければ、「アプリがやるべき仕事の手順」 と理解すると良い。
ビジネスロジックの具体例
「新規ユーザー登録」を考えてみる。やることは単に「DBにINSERTする」だけではない。
- 入力されたメールアドレスがすでに登録済みでないか確認する
- パスワードをハッシュ化する
- 問題なければDBにユーザーを保存する
- 登録完了メールを送る
この「手順(ルール)」こそがビジネスロジックだ。
// user.service.ts
export class UserService {
constructor(
private readonly userRepository: UserRepository,
private readonly mailer: MailerService,
) {}
async register(name: string, email: string, password: string): Promise<User> {
// 1. 重複チェック
const existing = await this.userRepository.findByEmail(email);
if (existing) {
throw new Error('このメールアドレスはすでに使用されています');
}
// 2. パスワードのハッシュ化
const hashedPassword = await bcrypt.hash(password, 10);
// 3. DBに保存
const user = await this.userRepository.create(name, email, hashedPassword);
// 4. メール送信
await this.mailer.sendWelcomeEmail(user.email);
return user;
}
}「重複チェックしてからINSERTする」「パスワードはハッシュ化する」は、このアプリ固有のビジネスのルールだ。
それをServiceクラスにまとめて書くことで、Controller側はシンプルに「Serviceに処理を投げるだけ」になる。
WebAPIや機能ごとにServiceを作る
サービスは機能・ドメイン単位で分割するのが基本だ。
graph TD
UserService["UserService<br/>ユーザー登録・更新・削除・検索"]
OrderService["OrderService<br/>注文受付・キャンセル・履歴参照"]
NotificationService["NotificationService<br/>メール・プッシュ通知送信"]
UserService --> UserRepo["UserRepository"]
OrderService --> OrderRepo["OrderRepository"]
OrderService --> UserRepo
NotificationService --> UserRepo
「ユーザーに関すること」は UserService、「注文に関すること」は OrderService というように分けることで、1つのクラスが肥大化するのを防げる。
トランザクションとサービス
複数のDB操作がセットでないと困る場面がある。
例)注文処理
1. ordersテーブルに注文を追加する
2. stocksテーブルの在庫を減らす1だけ成功して2が失敗したら、注文が入ったのに在庫が減らないというバグになる。
「全部成功するか、全部なかったことにするか」を保証する仕組みがトランザクションだ。
トランザクションはServiceで制御するのが原則だ。ServiceはRepositoryを呼ぶ起点であり、「どの操作をひとまとまりにするか」を知っているのがServiceだからだ。
// order.service.ts
async placeOrder(userId: number, itemId: number): Promise<Order> {
return await this.dataSource.transaction(async (manager) => {
// トランザクション内で複数の操作を行う
const order = await this.orderRepository.create(userId, itemId, manager);
await this.stockRepository.decrement(itemId, manager);
return order;
});
// エラーが起きたら自動的に両方ロールバックされる
}sequenceDiagram
participant S as OrderService
participant OR as OrderRepository
participant SR as StockRepository
participant DB as Database
S->>DB: トランザクション開始
S->>OR: 注文を作成
OR->>DB: INSERT INTO orders ...
S->>SR: 在庫を減らす
SR->>DB: UPDATE stocks SET quantity = quantity - 1 ...
Note over DB: 全て成功
S->>DB: コミット(確定)
Note over S,DB: もし途中でエラーが出たら
S->>DB: ロールバック(全操作をなかったことに)
まとめ:3つの役割を一言で
| 用語 | 一言で言うと |
|---|---|
| エンティティ(Entity) | DBテーブルをコードで表現した「データの器」 |
| リポジトリ(Repository) | テーブル単位でDB操作(CRUD)を担当する「DBとの窓口係」 |
| サービス(Service) | ビジネスのルール・手順を実装した「アプリの頭脳」。リポジトリを呼び出してDBにアクセスし、トランザクションも管理する |
graph TD
Service["🧠 Service<br/>(ビジネスロジック)<br/>・ルールのチェック<br/>・手順の制御<br/>・トランザクション管理"]
Repository["🗄 Repository<br/>(DB窓口)<br/>・CRUD操作<br/>・SQL実行"]
Entity["📦 Entity<br/>(データの器)<br/>・テーブル定義の写し<br/>・データの運び役"]
Service -->|「このデータを取ってきて」| Repository
Repository -->|「これがデータです」| Service
Repository -->|データを入れて運ぶ| Entity
Entity -->|Serviceで扱う| Service
この3つの分離によって、「DBの接続先が変わってもRepositoryだけ修正すればいい」「ビジネスルールが変わってもServiceだけ直せばいい」という保守しやすい構造になる。