Kohei Nozaki's blog 

JPA Builder パターン


Posted on Sunday Dec 04, 2016 at 12:00AM in Technology


このエントリは Java EE Advent Calendar 2016 の4日目の記事です.


JPA のおかげで,多数のカラムや複雑なリレーションを持つテーブルへのレコード生成は,プレーンな JDBC を使っていた時代に比べると,たいへん楽になりました.しかし,いぜん頭を悩ませる局面もあります.例えば,以下の図のようなスキーマを考えてみてください:

829fc6f6 cccb 44c2 816a b7f397e83309

このスキーマ中の Employee テーブルへレコードを生成するコードを考えてみてください.以下のようになると思います:

public class EmployeeService {

    private final EntityManager em;

    EmployeeService(final EntityManager em) {
        this.em = em;
    }

    public long create(long deptId,
                       String name,
                       boolean temporary,
                       Set<Long> projectIds,
                       Set<String> phoneNumbers) {

        // instantiating and setting attributes of employee
        final Employee employee = new Employee();
        employee.setName(name);
        employee.setTemporary(temporary);
        employee.setProjects(new HashSet<>());
        employee.setPhones(new HashSet<>());

        // making a relation between employee and dept
        final Dept dept = em.find(Dept.class, deptId);
        employee.setDept(dept);
        em.persist(employee);
        dept.getEmployees().add(employee);

        // making relations between employee and projects
        for (final Long projectId : projectIds) {
            final Project project = em.find(Project.class, projectId);
            project.getEmployees().add(employee);
            employee.getProjects().add(project);
        }

        // creating phones
        for (final String phoneNumber : phoneNumbers) {
            final Phone phone = new Phone();
            phone.setNumber(phoneNumber);
            phone.setEmployee(employee);
            em.persist(phone);
            employee.getPhones().add(phone);
        }

        em.flush(); // making sure a generated id is present

        return employee.getId();
    }
}

いま書いたメソッド create() を呼び出すコードは,以下のようになります:

final Set<Long> projectIds = new HashSet<>();
Collections.addAll(projectIds, project1Id, project2Id);
final Set<String> phoneNumbers = new HashSet<>();
Collections.addAll(phoneNumbers, "000-0000-0001", "000-0000-0002", "000-0000-0003");

final long savedEmployeeId = service.create(
        engineeringDeptId,
        "Jane Doe",
        true,
        projectIds,
        phoneNumbers);

悪くはありません.しかし,もっと複雑なリレーションや省略可能なカラムが多数存在するケースを考えてみてください.それらに対応するメソッドの引数も多くなるのにしたがって,省略可能な引数に対応するためのオーバーロードや null の引数が並ぶメソッド呼び出しも増殖していき,だんだんメンテナンスが大変になっていきます.

このようなケースでは,私が個人的に「JPA Builder パターン」と呼んでいる書き方がおすすめです.以下に示すような非 static のネストされた Builder クラスと,そのインスタンスを生成するためのメソッドを,前述の EmployeeService クラスに対して追加します:

...

public Builder builder(long deptId, String name) {
    return new Builder(deptId, name);
}

public final class Builder { // non-static
    private final long deptId;
    private final String name;
    private boolean temporary;
    private Set<Long> projectIds = new HashSet<>();
    private Set<String> phoneNumbers = new HashSet<>();

    private Builder(final long deptId, final String name) {
        this.deptId = deptId;
        this.name = name;
    }

    public Builder temporary(boolean temporary) {
        this.temporary = temporary;
        return this;
    }

    public Builder projectIds(Long... ids) {
        Collections.addAll(projectIds, ids);
        return this;
    }

    public Builder phoneNumbers(String... numbers) {
        Collections.addAll(phoneNumbers, numbers);
        return this;
    }

    public long build() {
        // In reality, passing "this" instead of actual values (deptId, name, ...) is recommended
        return EmployeeService.this.create(deptId, name, temporary, projectIds, phoneNumbers);
    }
}

これを呼び出すコードは,以下のようになります:

final long savedEmployeeId = service.builder(engineeringDeptId, "Jane Doe")
        .temporary(true)
        .projectIds(project1Id, project2Id)
        .phoneNumbers("000-0000-0001", "000-0000-0002", "000-0000-0003")
        .build();

今回の例のようにリレーションや省略可能なカラムの数がさほど多くない場合,あまりメリットがないように見えるかもしれませんが,現実には,もっと多い数になることは珍しくありません.そのようなケースでは,このパターンを使うことで保守性・可読性を維持・向上させることができます.

このエントリで使用したクラスとテストを含むサンプルの一式は以下にありますので,実際に動かしてみたい方はチェックしてみてください.EclipseLink と Hibernate での動作を確認しています.DB のセットアップなどは必要なく,単に "mvn test" を叩くとインメモリの Apache Derby 上でテストが走ります:

宣伝: 求人のご案内

現在,私の勤めている (株) L is B では,エンジニアを募集しています.主に "direct" という企業向けメッセンジャーの開発と運用を行っている会社です.

残念ながら,今のところ GlassFish や WildFly のようなフルの Java EE コンテナは使われていないのですが,Java EE の構成要素,例えば JPA や JAX-RS などはヘビーに使われています.「Java EE を使って自社サービス開発したい!」という方,ぜひ以下をチェックしてみてください: