🌻

Doma で many-to-many 型のリレーションシップを扱う(Criteria API版)

2025/02/11に公開

この記事では、DomaCriteria API を使って many-to-many 型のリレーションシップを扱う方法を紹介します。

SQLを使ってmany-to-many 型のリレーションシップを扱う方法は以下の記事を参照ください。
https://zenn.dev/nakamura_to/articles/3bd731d3c50fa5

DDL と初期データ

例えば、users、roles、users_roles の 3 つのテーブルがあるとしましょう。1 人のユーザーが複数のロールを持つことができ、1 つのロールが複数のユーザーに割り当てられることがある、というケースです(冒頭で紹介した記事で使ったものと同じです。)

CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE roles (
    id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE users_roles (
    user_id INT NOT NULL,
    role_id INT NOT NULL,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);

INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob');
INSERT INTO roles (id, name) VALUES (1, 'Admin'), (2, 'Editor'), (3, 'Viewer');

-- Alice は Admin と Editor の両方のロールを持つ
INSERT INTO users_roles (user_id, role_id) VALUES (1, 1), (1, 2);

-- Bob は Editor と Viewer の両方のロールを持つ
INSERT INTO users_roles (user_id, role_id) VALUES (2, 2), (2, 3);

エンティティクラス

usersrolesusers_roles の3つのテーブルに対応するエンティティクラスを作成します。

@Entity(metamodel = @Metamodel)
@Table(name = "users")
public class User {
  @Id public Integer id;
  public String name;
  @Association public List<Role> roles = new ArrayList<>();
}

@Entity(metamodel = @Metamodel)
@Table(name = "roles")
public class Role {
  @Id public Integer id;
  public String name;
  @Association public List<User> users = new ArrayList<>();
}

@Entity(metamodel = @Metamodel)
@Table(name = "users_roles")
public class UserRole {
    @Id public Integer userId;
    @Id public Integer roleId;
}

Criteira API のメタモデルを生成するために metamodel = @Metamodel の指定は必須です。

実行例

JUnitのテストから実行してみましょう。

  @Test
  public void manyToManyWithCriteriaApi(Config config) {
    QueryDsl queryDsl = new QueryDsl(config);

    // メタモデルをインスタンス化
    User_ u = new User_();
    Role_ r = new Role_();
    UserRole_ ur = new UserRole_();

    // クエリを構築して実行
    List<User> users =
        queryDsl
            .from(u)
            .leftJoin(ur, on -> on.eq(u.id, ur.userId))
            .leftJoin(r, on -> on.eq(ur.roleId, r.id))
            .orderBy(o -> o.asc(u.id))
            .associate(
                u,
                r,
                (user, role) -> {
                  user.roles.add(role);
                  role.users.add(user);
                })
            .fetch();

    // 結果を出力
    for (User user : users) {
      String userName = user.name;
      String roleNames =
          user.roles.stream().map(role -> role.name).collect(Collectors.joining(","));
      System.out.printf("user=%s, roles=%s%n", userName, roleNames);
    }
  }

出力結果は次のようになります。

user=Alice, roles=Admin,Editor
user=Bob, roles=Editor,Viewer

発行される SQL は次のとおりです。

select 
    t0_.ID, t0_.NAME, t2_.ID, t2_.NAME 
from 
    users t0_ 
    left outer join 
    users_roles t1_ on (t0_.ID = t1_.USER_ID) 
    left outer join 
    roles t2_ on (t1_.ROLE_ID = t2_.ID) 
order by t0_.ID asc

まとめ

Doma の Criteria API を使って many-to-many 型のリレーションシップを扱う例を紹介しました。

冒頭で紹介したように SQL を使っても同じことができます。SQL を使う方法は、SELECT リストの命名規則さえあっていればアグリゲートにマッピング可能なので、DBに特化した関数や複雑なサブクエリが必要な場合には向いているでしょう。

なお、SQL を使う方法と Criteria API を使う方法は混在しても全く問題ありません(例えば、JPAのように適切なタイミングでflushが必要などの注意点はありません)。システムやプロジェクトの特性に応じて適材適所で使ってもらえればと思います。

Discussion