LoginSignup
5

More than 1 year has passed since last update.

posted at

Spring Data JDBCで1対多テーブルを結合して取得する方法

はじめに

Java言語初学者ですが、SpringBootフレームワークを使用してSQLを操作する際に、
1対多のテーブルを結合した結果を取得する方法でハマったため、
1対1のテーブル結合の方法と、1対多のテーブル結合の方法を比較したメモとして残します。

環境

OS version framework 使用環境
windows10 Java11 spring boot Spring Tool Suite 4

・Java11でSpring Data JDBCを使用できる状態
・動作するデータベース用意済み
・1対多の関係でテーブルを2つ作成済み
・テーブルにデータを格納済み

※今回の説明では「clabs」、「members」で記載します。

※また今回はテーブル結合方法の説明の為必要最低限の記載だけします。(importなどは省略)

1対1のテーブルを結合して取得する方法

1対1のテーブルを結合してデータを取得する際は以下の方法を使用します。

※1対1のテーブル説明の為、1つのクラブには1人しか入れないこととします。

Domainクラス

データベースに登録しているカラムをフィールド変数として記載します。

Member.javaにはテーブル結合に必要なフィールド変数を追加しています。

Clab.java
public class clab {
  /** ID */
  private Integer id;
  /** クラブ名 */
  private String name;

  //以下にGetter,Setter,ToStringなどの記載
  ...
}
Member.java
public class member {
  /** ID */
  private Integer id;
  /** メンバー名 */
  private String name;
  /** クラブID */
  private Integer clabId;
  /** クラブ名 */
  private String clabName;

  //以下にGetter,Setter,ToStringなどの記載
  ...
}

Repositoryクラス

RowMapperを使用します。

ざっくり説明するとSQL実行時にDBから検索してきたResultSetオブジェクトをRowMapperに渡す動作をします。(1行分のデータを取得してくる)

MemberRepository.java
@Repository
public class ClabRepository {

  //データベース操作を行うための変数宣言
  @Autowired
  private NamedParameterJdbcTemplate template;

  //ResultSetオブジェクトに格納された1行分のデータをMember型の変数にセットしてreturnする
  private static final RowMapper<Member> MEMBER_ROWMAPPER = (resultSet,i) -> {
    Member member = new Member();
    member.setId(resultSet.getInt("m_id"))
    member.setString(resultSet.getString("m_name"));
    member.getClabId(resultSet.getInt("m_clab_id"));
    member.getClabName(resultSet.getString("c_name"));
    return member;
  }

  //SQLを実行して結果をreturnする
  public List<Member> findAllMemberAndClab() {
    String sql = "SELECT m.id as m_id, m.name as m_name, m.clab_id as m_clab_id, c.name as c_name FROM members as m LEFT OUTER JOIN clabs as c ON m.clab_id = c.id;";
    List<Member> memberList = template.query(sql, MEMBER_ROWMAPPER);
    return memberList;
  }
}

Repositoryクラスに記載したコードの流れとしては、SQLを実行したときに、フレームワークから検索してきたResultSetオブジェクトをRowMapperに渡す。

裏側で1行分のデータをResultSetオブジェクトに格納して、それらをmember変数に格納してmemberList変数に格納していく。

SQLで該当するデータをすべてmemberListに格納できたらreturnする。

1対多のテーブルを結合して取得する方法

1対多のテーブルを結合してデータを取得する際は以下の方法を使用します。

※1対多のテーブル説明の為、1つのクラブには複数人登録できることとします。

Domainクラス

データベースに登録しているカラムをフィールド変数として記載します。

Clab.javaにはmembersテーブルのデータを格納するためのList型の変数を追加しています。

Member.javaにはテーブル結合に必要なフィールド変数を追加しています。

Clab.java
public class clab {
  /** ID */
  private Integer id;
  /** クラブ名 */
  private String name;
  /** メンバーリスト */
  private List<Member> memberList;

  //以下にGetter,Setter,ToStringなどの記載
  ...
}
Member.java
public class member {
  /** ID */
  private Integer id;
  /** メンバー名 */
  private String name;
  /** クラブID */
  private Integer clabId;

  //以下にGetter,Setter,ToStringなどの記載
  ...
}

Repositoryクラス

ResultSetExtractorを使用します。

ざっくり説明するとSQL実行時にDBから検索してきたResultSetオブジェクトを自分でセットする必要があります。(複数行分のデータを取得してくる)

MemberRepository.java
@Repository
public class ClabRepository {

  //データベース操作を行うための変数宣言
  @Autowired
  private NamedParameterJdbcTemplate template;

  //ResultSetオブジェクトに格納された複数行分のデータをList<Clab>変数にセットしてreturnする
  private static final ResultSetExtractor<List<Clab>> CLAB_MEMBER_RESULTSET = (rs) -> {
    //初めにデータを格納するための変数を宣言
    List<Clab> clabList = new ArrayList<>();

    //メンバーを格納するためのList<Member>変数を宣言(値はNullを格納しておく)
    List<Member memberList = null;

    //clabsテーブルは結合した際に複数行にわたり同じデータが出力される可能性があるため、前のClabテーブルのIDを保持するための変数を宣言
    int beforeIdNum = 0;

    //ResultSetオブジェクトに格納された複数のデータをList<Clab>変数に格納していく
    while(rs.next()) {
      //現在検索しているClabテーブルのIDを格納するための変数を宣言
      int nowIdNum = rs.getInt("c_id");

      //現在検索しているClabテーブルのIDと前のClabテーブルのIDが違う場合は新たにClabオブジェクトを作成する
      if (nowIdNum != beforeIdNum) {
        Clab clab = new Clab();
        clab.setId(nowIdNum);
        clab.setName(rs.getString("c_name"));
        //メンバーがいた際にClabオブジェクトのmemberListに格納するため空のArrayListをセットしておく
        memberList = new ArrayList<Member>();
        clab.setMemberList(memberList);
        clabList.add(clab);
      }

      //ClabにMemberがいない場合はMemberオブジェクトを作成しないようにする
      if (rs.getInt("m_id") != 0) {
        Member member = new Member();
        member.setId(rs.getInt("m_id"));
        member.setName(rs.getString("m_name"));
        //memberをclabオブジェクト内にセットされているmemberListに直接追加する
        memberList.add(member);
      }

      //現在検索しているClabテーブルのIDを前のClabテーブルのIDを入れるbeforeIdNumに代入する
      beforeIdNum = nowIdNum;
    }
    return clabList;
  }

  public List<Clab> findAll() {
    String sql = "SELECT c.id as c_id, c.name as c_name, m.id as m_id, m.name as m_name FROM clabs as c LEFT OUTER JOIN members as m ON c.id = m.clab_id;";
    List<Clab> clabList = template.query(sql, CLAB_MEMBER_RESULTSET);
    return clabList;
  }
}

Repositoryクラスに記載したコードの流れとしては、SQLを実行したときに、フレームワークから検索してきたResultSetオブジェクトをResultSetExtractorに渡す。

その際にSQL実行した際に検索したすべての行をResultSetオブジェクトに格納するため、

ResultSetオブジェクトのnext()メソッドを使用して、オブジェクトにデータがある間ループ処理を行うようにする。

clabsテーブルは取得したデータ内容が重複することがあるため同じIDの場合はオブジェクトを生成しないようにする。

ResultSetオブジェクトにデータがなくなった際ループ処理が終了して、memberListをreturnする。

まとめ

1対多のテーブル結合をRowMapperを使用してClabRepositoryとMemberRepositoryに分けて実施することで再現することができるのですが、SQLを何回も実施しないといけなくなるので、実行時間が長くなってしまいます。そのためテーブルの結合をする際はResultSetExtractorを使用したほうがいいといえます。

参考文献

以下のGitHubを参考にさせていただきました。
GitHub

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
What you can do with signing up
5