SpringBoot3✖️SpringDataJpaでDBのデータを取得する

SpringBoot3(REST API)✖️Next.jsで作るSNSWebアプリケーション

今パートの目標

前回はデータベース接続の第二弾としてSpringBoot3✖️FlyWayでPostgresqlDBのマイグレーションを作成しました。
ここまでで準備は完了したので今回はSpringBoot3✖️SpringDataJpaでDBのデータを取得し、フロントエンドで表示してみたいと思います。

SpringBootからusersテーブルを取得する

SpringBootからusersテーブルのデータを取得するために、まずusersテーブルにテスト用のレコードを作成します。

テストデータを作成する

テストデータの作成は今回はpgadmin4から単純にレコードを挿入していきます。

まず下記コマンドでサーバーを起動しましよう。

share-favplace-api % docker-compose up -d

次に下記URLにアクセスし、pgadmin4にログインします。
http://localhost:8888/

ログインできたらusersテーブル → スクリプト → INSERTスクリプトを選択すると、INSERTスクリプトが表示されるのでVALUESの?に値を入れていきます。

NOT NULL制約のついていないカラムにだけ値を入れて実行します。

INSERT INTO public.users(
	id, username, email, password)
	VALUES (1, 'testuser', 'test.email@gmail.com', 'password');

Userクラスを作成する

それではSpringBootからusersテーブルのデータを取得していきます。
まず、usersテーブルに対応するuserエンティティのクラスを作成します。

- src
  └ main
    └ src
      └ java
        └ entity
      └ User.java
package com.pandaman.sharefavplaceapi.entity;


import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users")
public class User {
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "users_id_seq")
    @SequenceGenerator(name = "users_id_seq", sequenceName = "users_id_seq", allocationSize = 1)
    @Id
    @Column(name = "id")
    private Integer id;
    @Column(name = "username")
    private String username;
    @Column(name = "email")
    private String email;
    @Column(name = "password")
    private String password;
    @Column(name = "activated")
    private Boolean activated;
}

簡単に説明します。

@Entity

このクラスがエンティティクラスであることを表します。

@Table

このクラスがテーブルに対応したクラスであることを表します。

@Id

プライマリキーとなるカラムを指定します。

@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "users_id_seq")
@SequenceGenerator(name = "users_id_seq", sequenceName = "users_id_seq", allocationSize = 1)

によって自動採番の方法を指定しています。詳しくは以下を参照してください。

GeneratedValue (Jakarta EE Platform API) - Javadoc
Java アプリケーションサーバの標準規格および API を定めた Jakarta EE (Enterprise Edition) 仕様 API Javadoc 日本語ドキュメント。随時、最新版の内容が反映されます。
SequenceGenerator (Jakarta EE 8 仕様 API) - Javadoc
Java アプリケーションサーバの標準規格および API を定めた Jakarta EE (Enterprise Edition) 仕様 API Javadoc 日本語ドキュメント。随時、最新版の内容が反映されます。
@Column

データベースのカラムであることを表します。

@Getter

lombokのアノテーションです。宣言されたメンバ変数に対するgetterを自動生成してくれます。

@Setter

lombokのアノテーションです。宣言されたメンバ変数に対するSetterを自動生成してくれます。

@NoArgsConstructor

lombokのアノテーションです。デフォルトコンストラクタを自動生成してくれます。

@AllArgsConstructor

lombokのアノテーションです。宣言された全てのメンバ変数を引数に取るコンストラクタを自動生成してくれます。

抽象エンティティクラスを作成する

usersテーブルに対するエンティティクラスが作成できましたが、レコード作成日時と更新日時のカラムがありません。
このカラムはすべてのテーブルに付与する予定のため、抽象クラスとして作成し各エンティティクラスで継承するようにしていきましょう。

- src
  └ main
    └ src
      └ java
        └ entity
      └ AbstractEntity.java
package com.pandaman.sharefavplaceapi.entity;


import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;
/**
 * DB共通Entity
 */

@Getter
@Setter
@MappedSuperclass
public abstract class AbstractEntity {
    /** 登録日時 */
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at", nullable = false, updatable = false)
    private Date createdAt;

    /** 更新日時 */
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "updated_at", nullable = false)
    private Date updatedAt;

    /** 更新日時の設定 */
    @PreUpdate
    public void onPreUpdate() {
        setUpdatedAt(new Date());
    }
}
@MappedSuperclass

このアノテーションを付与して各Entityクラスに継承することで、共通のカラムを派生Entity先クラスにマッピングします。

@Temporal

日付項目をマッピングする場合に使用します。
@Temporal の値には、データベース上の詳細な型(DATETIMETIMESTAMP)を指定します。

@PreUpdate

このアノテーションを付与することで更新前に毎回実行されます。
今回の場合は更新時は毎回更新日時を更新するようにしています。

※登録に関しては@PrePersistというアノテーションで登録前に実行もできますが、Postgresqlでデフォルトを設定したので今回は更新前だけにしています。

抽象エンティティクラスを継承する

それではUserクラスでAbstractEntityクラスを継承しましょう。

public class User extends AbstractEntity {

これでエンティティクラスの用意は完了です。

リポジトリインターフェースを作成する

次にリポジトリクラスクラスを作成してJavaコードからデータベースにアクセスしていきたいと思います。

- src
  └ main
    └ src
      └ java
        └ repository
      └ UserRepository.java
package com.pandaman.sharefavplaceapi.repository;


import com.pandaman.sharefavplaceapi.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Integer> {
}

JpaRepositoryインターフェースを実装することにより、基本的なCRUD操作を実行するメソッドが使用できるようになります。

一例
save(S entity)指定されたエンティティを保存します。
delete(T entity)指定されたエンティティを削除します。
deleteById(ID id)指定された ID を持つエンティティを削除します。
findById(ID id)ID によってエンティティを取得します。
existsById(ID id)指定された ID を持つエンティティが存在するかどうかを返します。
findAll()すべてのエンティティを返します。
JpaRepository (Spring Data JPA Parent 3.2.5 API) - Javadoc
Spring Boot の概要から各機能の詳細までが網羅された公式リファレンスドキュメントです。開発者が最初に読むべきドキュメントです。

サービスクラスを作成する

それではビジネスロジックを実装するサービスクラスを作成し、先ほどのリポジトリインターフェースのfindAllメソッドを使用してDBからデータを取得していきます。

- src
  └ main
    └ src
      └ java
        └ service
      └ UserService.java
          └ UserServiceImpl.java

まずはUserServiceインターフェースを作成します。

package com.pandaman.sharefavplaceapi.service;

import com.pandaman.sharefavplaceapi.entity.User;

import java.util.List;

public interface UserService {
    /**
     * ユーザー全件取得
     *
     * @return List<User>
     */
    List<User> findAll();
}

次にUserServiceインターフェースを実装するUserServiceImplクラスを作成し実装していきます。

package com.pandaman.sharefavplaceapi.service;

import com.pandaman.sharefavplaceapi.entity.User;
import com.pandaman.sharefavplaceapi.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional
public class UserServiceImpl implements UserService{
    private final UserRepository userRepository;

    @Override
    public List<User> findAll() {
        return userRepository.findAll();
    }
}

サービスクラスにfindAllというメソッドを作成し、そこからリポジトリインターフェースのfindAllメソッドを呼び出しています。
ビジネスロジックを実装するクラスとDBにアクセスするクラスを分けるためにこのような構成にしています。

@RequiredArgsConstructor

lombokのアノテーションです。
このアノテーションを付与することによってfinal修飾子のついた定数を引数に取るコンストラクタを生成してくれます。
SpringBootでは@RequiredArgsConstructorをつけると@Autowiredアノテーションを付与しなくても定数にインジェクションしてくれようになっています。

コントローラークラスを編集する

ここまででDBから値を取得する準備はできたので最後にコントローラークラス(入口のクラス)を編集し、実際にユーザーテーブルを取得してみましょう。

HelloContollerはもう必要ないので、HelloContollerを編集します。

package com.pandaman.sharefavplaceapi.controller;

import com.pandaman.sharefavplaceapi.entity.User;
import com.pandaman.sharefavplaceapi.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @GetMapping("users")
    public List<User> getUsers() {
        return userService.findAll();
    }
}

これで、http://localhost:8080/api/v1/usersでユーザー一覧を取得することができるようになりました。
SpringBootではRestControllerでList<Object>やObjectを返すメソッドを作成すると自動的にjson形式に変換してレスポンスを返してくれます。便利ですね。

PostmanからAPIを叩く

それでは実際にPostmanからAPIを叩いてユーザー一覧を取得してみましょう。
まずは忘れずにサーバーを起動しておきましょう。

share-favplace-api % docker-compose up -d

それではPostmanにログインして「http://localhost:8080/api/v1/users」にリクエストを送信します。

[
    {
        "createdAt": "2024-04-07T08:33:03.259+00:00",
        "updatedAt": "2024-04-07T08:33:03.259+00:00",
        "id": 1,
        "username": "testuser",
        "email": "test.email@gmail.com",
        "password": "password",
        "activated": false
    }
]

このようにJSON形式のレスポンスが取得できていれば成功です。

フロントエンドからユーザーテーブルを取得する

それでは最後にフロントエンドからAPIを叩いてユーザーテーブルを取得してみましょう。
まずはユーザーオブジェクトの型を定義します。

- src
  └ types
    └ User.ts
export interface User {
  id: number;
  username: string;
  email: string;
  password: string;
  activated: boolean;
}

次に、App.tsxでユーザー一覧を取得するようにコードを修正します。

import { useState } from 'react';
import axios from './configs/axios';
import { User } from './types/User';

export default function App() {
  const [users, setUsers] = useState(Array<User>);

  const handleClick = () => {
    axios.get('/api/v1/users')
      .then((res) => {
        setUsers(res.data)
        console.log(res.data[0].activated);
      })
  };

  return (
    <div>
      <div className='flex justify-center mt-10'>
        <button type="button" className="text-white bg-gradient-to-r from-purple-500 to-pink-500 hover:bg-gradient-to-l focus:ring-4 focus:outline-none focus:ring-purple-200 dark:focus:ring-purple-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2" onClick={() => handleClick()}>Click!</button>
      </div>
      <div className="flex justify-center mt-5">
        <h1 className="text-3xl font-bold underline">
          <div className="relative overflow-x-auto shadow-md sm:rounded-lg">
            <table className="w-full text-sm text-left rtl:text-right text-blue-100 dark:text-blue-100">
              <thead className="text-xs text-white uppercase bg-blue-600 dark:text-white">
                <tr>
                  <th scope="col" className="px-6 py-3">
                    ID
                  </th>
                  <th scope="col" className="px-6 py-3">
                    Username
                  </th>
                  <th scope="col" className="px-6 py-3">
                    Email
                  </th>
                  <th scope="col" className="px-6 py-3">
                    Password
                  </th>
                  <th scope="col" className="px-6 py-3">
                    Activated
                  </th>
                </tr>
              </thead>
              <tbody>
                {users.map((user) => (
                  <tr className="bg-blue-500 border-b border-blue-400" key={user.id}>
                    <th scope="row" className="px-6 py-4 font-medium text-blue-50 whitespace-nowrap dark:text-blue-100">
                      { user.id }
                    </th>
                    <td className="px-6 py-4">
                      { user.username }
                    </td>
                    <td className="px-6 py-4">
                      { user.email}
                    </td>
                    <td className="px-6 py-4">
                      { user.password }
                    </td>
                    <td className="px-6 py-4">
                      { user.activated ? '○' : '×'}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>

        </h1>
      </div>
    </div>
  )
}

ユーザー一覧を取得してテーブル形式で表示するようにしています。

それではフロントエンドで表示してみましょう。
まずは忘れずにフロントエンドのサーバーを立ち上げます。

share-favplace-front % docker-compose up -d

サーバーが立ち上がったらhttp://localhost:3000にアクセスします。

するとこのような画面になっていると思います。
ではボタンを押してユーザー情報を取得します。

いい感じに取得できていますね!☺️

まとめ

今回でフロントエンドからリクエストを送り、SpringBootでDBから値を取得してレスポンスを返し、フロントエンドで表示するという一連の流れが完成しました。

次回はRender.comでPostresqlの作成、環境変数の設定をおこない、本番環境でも同じように動作確認ができるようにしていきたいと思います。

コメント

タイトルとURLをコピーしました