はじめに
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にはテーブル結合に必要なフィールド変数を追加しています。
public class clab {
  /** ID */
  private Integer id;
  /** クラブ名 */
  private String name;
  //以下にGetter,Setter,ToStringなどの記載
  ...
}
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行分のデータを取得してくる)
@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にはテーブル結合に必要なフィールド変数を追加しています。
public class clab {
  /** ID */
  private Integer id;
  /** クラブ名 */
  private String name;
  /** メンバーリスト */
  private List<Member> memberList;
  //以下にGetter,Setter,ToStringなどの記載
  ...
}
public class member {
  /** ID */
  private Integer id;
  /** メンバー名 */
  private String name;
  /** クラブID */
  private Integer clabId;
  //以下にGetter,Setter,ToStringなどの記載
  ...
}
Repositoryクラス
ResultSetExtractorを使用します。
ざっくり説明するとSQL実行時にDBから検索してきたResultSetオブジェクトを自分でセットする必要があります。(複数行分のデータを取得してくる)
@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