8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

#はじめに
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

8
6
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
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?