0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【学習】Spring boot 難易度3「Caching Data with Spring」

Last updated at Posted at 2025-09-27

はじめに

今回も引き続きSpringチュートリアルの内容をまとめていきたいと思います

開発環境やIDEAについては下記記事参照

Caching Data with Spring(Spring でデータキャッシング)

日本語サイト

英語サイト

★キャッシュとは
Webサイトやアプリの表示速度を速くするために、一時的にデータを使用しているデバイスに保存する仕組み

1:Create a Book Model(ブックモデルを作成する)

  • 「本の情報を扱うための設計図(モデル)」をJavaのコードにしたのが「Bookクラス」
package com.example.caching;

// Bookクラス、「本」というデータの設計図(モデル)
public class Book {
    
    private String isbn; // 本の識別番号を保存する変数
    private String title; // 本のタイトルを保存する変数

    // コンストラクタ。newした際に自動的に呼び出されるメソッド
    public Book(String isbn, String title) {

        // 左のthis.isbnはこのクラスのフィールド(変数)、右のisbnは外から渡される値(引数)
        this.isbn = isbn;
        this.title = title;
    }

    // isbnの値を取得(取り出す)するためのメソッド
    public String getIsbn() {
        return isbn;
    }

    // isbnの値を設定(変更)するためのメソッド
    public String setIsbn(String isbn) {
        this.isbn = isbn;
    }

    // titleの値を取得(取り出す)するためのメソッド
    public String getTitle() {
        return title;
    }

    // titleの値を設定(変更)するためのメソッド
    public String setTitle(String title) {
        this.title = title;
    }

    // このBookオブジェクトの中身を文字列に変換して返すメソッド
    @Override //これは書かなくても動くらしい
    public String toString(){
        return "Book{" + "isbn='" + isbn + '\'' +", title='" + title + '\'' + '}';
    }
}

★コンストラクタ部分について

Book myBook = new Book("1234567890", "Java入門");
  • 例えば使う側で以下のように記述
    "1234567890" が引数の isbn
    "Java入門" が引数の title
    としてコンストラクタに渡され、this.isbnthis.title にそれぞれセットされる

★本の更新や取得について、具体例

Book book1 = new Book("978-1234567890", "Spring入門");
Book book2 = new Book("978-0987654321", "Java完全版");

例:book1のタイトルを取得したい時

String title1 = book1.getTitle();
System.out.println(title1);

結果は "Spring入門"

例:book2のisbnを取得したい時

String isbn2 = book2.getIsbn();
System.out.println(isbn2);

結果は"978-0987654321" 

例:book1のタイトルを更新

book1.setTitle("Spring完全版");

toString() メソッドを書かないとどうなるのか

  • このメソッドは文字列として表現するためのモノ
  • なぜ使うかについて、System.out.println(book1); のようにオブジェクトを出力すると、Javaは自動的にそのオブジェクトのtoString()メソッドを呼び出しているから
定義されていない時
public class Book {
    private String isbn;
    private String title;

    public Book(String isbn, String title) {
        this.isbn = isbn;
        this.title = title;
    }

    // ★ここにtoString()がない!

    public static void main(String[] args) {
        Book book1 = new Book("978-1234567890", "Spring入門");
        System.out.println(book1);  // 👈 デフォルト表示「Book@1b6d3586  ← クラス名 + ハッシュコード」
    }
}
定義されている時
public class Book {
    private String isbn;
    private String title;

    public Book(String isbn, String title) {
        this.isbn = isbn;
        this.title = title;
    }

    // toStringを定義
    @Override
    public String toString() {
        return "Book{isbn='" + isbn + "', title='" + title + "'}";
    }

    public static void main(String[] args) {
        Book book1 = new Book("978-1234567890", "Spring入門");
        System.out.println(book1);  // 👈 文字列表示される!→ Book{isbn='978-1234567890', title='Spring入門'}
    }
}

 

★オーバーライドについて

  • すべてのJavaクラスは暗黙的にObjectクラスを継承している。
  • Object クラスには toString() メソッドがある
  • Bookクラスは Object クラスの toString() を「自分用に書き換えている(オーバーライドしている)」

★isbnとは

書籍を識別するための国際的なコード
国際標準図書番号ともいう(International Standard Book Number)

2:Create a Book Repository(ブックリポジトリを作成する)

ここでは、モデル(Book)を操作する役割を持つ
※interfaceとimplement合わせてリポジトリ

★インターフェース部分(リポジトリ設計図)

package com.example.caching;

// Book Repositoryを使うクラスは、String型のisbnをもらって、Book型のオブジェクトを返す
public interface BookRepository {
    // Isbnを使って本を取得する。isbnは引数。外から渡された値。
    Book getByIsbn(String isbn);
}
  • 「このインターフェースを使うなら、getByIsbn(String isbn) という機能を絶対に持っててね!」というルール。

★実装部分(その設計図に実際の動作を定義したもの)

package com.example.caching;

import org.springframework.stereotype.Component;

@Component
public class SimpleBookRepository implements BookRepository {

  @Override
  public Book getByIsbn(String isbn) {
    // わざと遅延を入れる処理
    simulateSlowService();
    return new Book(isbn, "Some book");
  }

  // Don't do this at home
  private void simulateSlowService() {
    try {
      long time = 3000L;
      Thread.sleep(time);
    } catch (InterruptedException e) {
      throw new IllegalStateException(e);
    }
  }
}

★implementとは

  • interface(インターフェイス)とは?→ 「こういう機能があるよ」と決めたルール(契約書)
  • implements(実装する)とは?→ そのルール通りに、ちゃんと中身(行動)を書くこと

★リポジトリかどうかの判断

  • 名前に Repository が含まれている
  • interface になっていることが多い
  • データを取得・保存するメソッドがある

Using the Repository(リポジトリの使用)

次に、リポジトリを接続し、それを使用していくつかの書籍にアクセスする必要がある。

src/main/java/com/example/caching/CachingApplication.javaのキャッシュアノテーションを有効にする前のコードは割愛。

package com.example.caching;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component  // アプリ起動時に自動的に実行されるクラスとして登録
public class AppRunner implements CommandLineRunner {

  private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);

  private final BookRepository bookRepository;

  // DI(依存性注入):Springがリポジトリのインスタンスを自動で渡してくれる
  public AppRunner(BookRepository bookRepository) {
    this.bookRepository = bookRepository;
  }

  @Override
  public void run(String... args) throws Exception {
    // アプリ起動後に自動で呼ばれるメソッド
    logger.info(".... Fetching books");  // ログ出力(標準出力ではなくログ)

    // 本を取得する処理(同じISBNでも何度も呼び出す)
    logger.info("isbn-1234 --> " + bookRepository.getByIsbn("isbn-1234"));
    logger.info("isbn-4567 --> " + bookRepository.getByIsbn("isbn-4567"));
    logger.info("isbn-1234 --> " + bookRepository.getByIsbn("isbn-1234"));
    logger.info("isbn-4567 --> " + bookRepository.getByIsbn("isbn-4567"));
    logger.info("isbn-1234 --> " + bookRepository.getByIsbn("isbn-1234"));
    logger.info("isbn-1234 --> " + bookRepository.getByIsbn("isbn-1234"));
  }
}
  • Spring Boot アプリが起動すると、AppRunner.run() が自動で呼び出される
  • 中で bookRepository.getByIsbn() を使って、何度も本を検索
  • 同じ ISBN(例: "isbn-1234")でも何度も検索
  • この時、キャッシュが無効だと毎回3秒かかる → キャッシュを有効にすると2回目以降はすぐ返ってくる!

Enable caching(キャッシュを使用可能にする)

SimpleBookRepository でキャッシュを有効にして、ブックが books キャッシュ内にキャッシュされるようにすることができる

重い処理を持つリポジトリ(本の情報を返す)
package com.example.caching;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

@Component  // DI登録(Springに「使う部品です」と知らせる)
public class SimpleBookRepository implements BookRepository {

  @Override
  @Cacheable("books")  // キャッシュ対象のメソッド。「books」という名前のキャッシュに保存
  public Book getByIsbn(String isbn) {
    simulateSlowService();  // 3秒間待つ(遅い処理)
    return new Book(isbn, "Some book");  // 同じISBNなら同じ本を返す
  }

  // わざと遅くするメソッド(実際にはAPIやDBアクセスを想定)
  private void simulateSlowService() {
    try {
      Thread.sleep(3000L); // 3秒スリープ
    } catch (InterruptedException e) {
      throw new IllegalStateException(e);
    }
  }
}

キャッシュアノテーションの処理を有効にする必要がある

アプリのエントリーポイント(起動クラス)src/main/java/com/example/caching/CachingApplication.java
package com.example.caching;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication  // SpringBoot用の便利なアノテーション(下記3つの機能をまとめている)
@EnableCaching          // キャッシュ機能を有効にする
public class CachingApplication {

  public static void main(String[] args) {
    // アプリケーションを起動する
    SpringApplication.run(CachingApplication.class, args);
  }
}

@ SpringBootApplicationの中身

  • @ Configuration:設定クラスですと伝える
  • @ EnableAutoConfiguration:Springが自動的に設定(たとえばWebアプリならサーバー起動)
  • @ ComponentScan:同じパッケージ内の他のクラスを探して登録(DI対象に)

?@Cacheable をどちらから先に追加すべき?順番に決まりはあるのか?

  • 順番に「厳密な決まり」はないけど、考え方はある
  • 1:@EnableCaching を追加(アプリ全体に「キャッシュ使いますよ」と知らせる)
  • 2:@Cacheable を追加(どこでキャッシュを使うか指定)
     ↓
    @EnableCaching がないと、@Cacheable は無視される

?「どのメソッドに @Cacheable を付けるべきか?」の考え方

  • 何度も同じ入力で呼ばれて、結果が変わらない処理に対して使うのが基本。
  • つまり、入力(引数)に対して結果(戻り値)が同じで、しかも処理が重い(時間がかかる)なら、キャッシュする価値がある。
     ↓
    今回でいうと下記部分が対象
@Override
public Book getByIsbn(String isbn) {
  simulateSlowService();
  return new Book(isbn, "Some book");
}

 ↓なぜこのメソッドなのか?

条件 該当しているか 説明
引数がある String isbn:入力が明確
出力が同じ 同じISBNなら同じ本が返る:new Book(isbn, "Some book")
時間がかかる Thread.sleep(3000) で遅い:パフォーマンスに影響あり
副作用がない データを変更していない:安心してキャッシュできる

?「"books" という文字列がキャッシュの保存先(Mapのキー名)になること」について

@Cacheable("books")
public Book getByIsbn(String isbn)

「このメソッドが呼ばれたとき、結果をキャッシュ(=保存)しておいて、同じISBNでもう一度呼ばれたら、保存された結果をすぐ返してね!」そして、「そのキャッシュは books という名前の場所に入れておいてね!」
 ↓
キャッシュは内部的に「Map(辞書)」のような構造で保存(この Map に「ISBN → Book の情報」という形で保存)

Map<String, Book> booksCache = new HashMap<>();

 ↓
具体例:ISBNで本を探すとき

bookRepository.getByIsbn("isbn-1234");

‼️このとき最初の呼び出しではキャッシュに何もないので以下のような感じになります
"books" キャッシュ領域Mapみたいなもの:

 見つからない  実際に処理実行  Bookオブジェクト作成  保存

保存後の状態
books = {
  "isbn-1234": Book{isbn='isbn-1234', title='Some book'}
}

‼️次に同じ ISBN を呼ぶ
bookRepository.getByIsbn("isbn-1234");
 ↓すでにキャッシュがある状態なので
"books" キャッシュ領域

 isbn-1234 はある  キャッシュから即返却
だからsimulateSlowService() の遅延処理がスキップされる

ここで「books」という名前の役割について

キャッシュの中には、複数のグループを作れ、この「グループ名」が "books"。つまり、「キャッシュをどこに保存するか」を区別する名前

books = { "isbn-1234": Book1, "isbn-5678": Book2 }
users = { "user-001": User1, "user-002": User2 }
orders = { "order-01": Order1 }

↓例えると
「books」はキャッシュの"引き出しの名前"
その中に「isbn」というキーで本の情報をしまっている
 ↓
キャッシュの全体:
└─ books 引き出し(Mapみたいな場所)
   ├─ "isbn-1234" : BookA
   ├─ "isbn-5678" : BookB

└─ users 引き出し
   ├─ "user-001" : UserX
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?