137

この記事は最終更新日から1年以上が経過しています。

@ts7i

段階的に理解する O/R マッピング

はじめに

O/R マッピングとは

O/R マッピングとは、一言で言えば、オブジェクト指向プログラミング言語においてリレーショナルデータベースのレコードを通常のオブジェクトとして操作する方法である。より詳細な定義を述べるより、実際のコードを見たほうがわかりやすいだろう。以下に、低レベルの JDBC API の利用例と、高レベルの O/R マッピングフレームワークの代表格である JPA の利用例を挙げる。

public List<Issue> findByProjectId(long projectId) {
  String query = "select id, title, description from issue where project_id = ?";
  try (PreparedStatement ps = connection.prepareStatement(query)) {
    ps.setLong(1, projectId);
    List<Issue> issues = new ArrayList<>();
    try (ResultSet rs = ps.executeQuery()) {
      while (rs.next()) {
        Issue issue = new Issue();
        issue.setId(rs.getLong("id"));
        issue.setTitle(rs.getString("title"));
        issue.setDescription(rs.getString("description"));
        issues.add(issue);
      }
    }
    return issues;
  } catch (SQLException e) {
    throw new RuntimeException(e);
  }
}
public List<Issue> findByProjectId(long projectId) {
  String query = "select i from Issue i where i.project.id = ?1";
  List<Issue> issues = entityManager.createQuery(query, Issue.class)
      .setParameter(1, projectId).getResultList();
  return issues;
}

両者を見比べれば違いは明らかである。高レベルの O/R マッピングを利用した後者では、定型的な記述が不要となり、より意図が明確に表現されている。

誤解される O/R マッピング

上記例ではいいことずくめに見える O/R マッピングだが、世には多くの不満の声がある。中には O/R マッピング全否定のような過激な立場もあれば、高レベルの O/R マッピングフレームワークを否定してよりシンプルな代替選択肢の利用を好む立場もある。そのような状況で高レベルの O/R マッピングフレームワーク利用を積極的に推し進める立場はむしろ少数派に見える。

なぜ O/R マッピングが嫌われるのか、そこには大きく分けて二つの理由があると考えられる。まず一つ目の理由は、高レベルの O/R マッピングフレームワークがプログラマの言うことを聞かないように見えることだろう。水面下で意図通りの SQL が実行されず、パフォーマンス問題への対応に苦労した経験のある O/R マッピングフレームワーク利用経験者は多いはずだ。この根底には O/R マッピングの基本的な機構に関する誤解があると思われる。次に二つ目の理由は、高レベルの O/R マッピングがしばしばそれに不向きなプロジェクトで利用されていることだろう。後で詳しく述べるように、スキーマへの裁量などの前提条件を満たさない状況で高レベルの O/R マッピングフレームワークを利用するのは自殺行為に近い。この根底には O/R マッピングの使い分け基準に関する誤解があると思われる。

この記事の概要

この記事では、上記のような誤解を解くために、一口に O/R マッピングと言っても複数のレベルがあることを示す。低レベルから高レベルの手段を段階的に見ていくことで、各レベルの基本的な機構がどのような課題への解決策として登場したのか、また各レベルの手段をどのような基準で使い分けるべきかが理解できるはずだ。

5 つのレベル

この記事では、O/R マッピングを以下の 5 つのレベルに分けて解説する。

  • レベル 1: 低レベル API
  • レベル 2: 前後処理の抽象化
  • レベル 3: クエリと単純なオブジェクトのマッピング
  • レベル 4: クエリと関連ナビゲーション可能なオブジェクトのマッピング
  • レベル 5: テーブルとオブジェクトのマッピング

これらのレベル設定はあくまで説明の便宜上のものである。Java の各種 O/R マッピングフレームワークの機能は実際には複数のレベルにオーバーラップしている。また、説明を簡潔にするため、対象の処理種別は参照系に絞り、更新系については省略する。

題材としては実際の各種 Java フレームワークを扱うため、それらの簡単な紹介記事としても読めるはずだ。ただし、機能の網羅的な説明は意図していないため、詳しく知りたい場合はリンク先の公式ドキュメントを参照してほしい。

レベル 1: 低レベル API

まず最初に、JDK 組み込みの JDBC API をそのまま利用したデータアクセスについて見てみよう。例としてこの記事冒頭に挙げたコードを再掲する。

public List<Issue> findByProjectId(long projectId) {
  String query = "select id, title, description from issue where project_id = ?";
  try (PreparedStatement ps = connection.prepareStatement(query)) {
    ps.setLong(1, projectId);
    List<Issue> issues = new ArrayList<>();
    try (ResultSet rs = ps.executeQuery()) {
      while (rs.next()) {
        Issue issue = new Issue();
        issue.setId(rs.getLong("id"));
        issue.setTitle(rs.getString("title"));
        issue.setDescription(rs.getString("description"));
        issues.add(issue);
      }
    }
    return issues;
  } catch (SQLException e) {
    throw new RuntimeException(e);
  }
}

題材はシンプルな課題管理アプリケーションである。上記コードの処理内容は、課題のプロジェクト ID による抽出である。

issue テーブルの定義は以下のようなものだ。

create table issue
(
   id bigint primary key,
   project_id bigint,
   title varchar (100),
   description text
);

利用方法

このレベルの手法を利用するために必要なことは以下の通りである。

  • クエリ文字列を指定する
  • クエリパラメータを指定する
  • クエリを実行する
  • クエリ結果をループで走査する
  • レコードをオブジェクトへ詰め替える
  • リソースを管理する
  • 低レベルの例外発生に対応する

課題

定型的な記述の煩雑さは問題である。コードを書く側としては、クエリを実行して結果を取ってくるだけのコードにしては記述量が多すぎる。コードを読む側としても、意図が余計なコードに埋もれてわかりにくい。

さらに、リソース管理上のリスクも見逃せない。上記例で try-with-resources を使って対応しているようなクローズ処理を忘れると、リソースリークが発生する。

選択基準

2019 年現在、プロダクションコードでこのレベルの手法を採用すべき場面はほとんどない。極度に性能を重視する場合や、何らかの事情でフレームワークの使用に制限がかかる場合に限って、このレベルの手法を使う機会があるかも知れない。ただしそれらの場合も、後述のレベル 2 に相当する手法を自前で実現することは容易だ。

レベル 2: 前後処理の抽象化

レベル 1 の定型的な記述のうち、前後処理に関するものは比較的簡単に抽象化できる。以下は Jdbi を利用した例だ。

public List<Issue> findByProjectId(long projectId) {
  String query = "select id, title, description from issue where project_id = ?";
  List<Issue> issues = handle.createQuery(query).bind(0, projectId)
      .map((rs, ctx) -> {
        Issue issue = new Issue();
        issue.setId(rs.getLong("id"));
        issue.setTitle(rs.getString("title"));
        issue.setDescription(rs.getString("description"));
        return issue;
      }).list();
  return issues;
}

利用方法

このレベルの手法を利用するために必要なことは以下の通りである。レベル 1 に比べると明らかに削減されている。

  • クエリ文字列を指定する
  • クエリパラメータを指定する
  • クエリを実行する
  • レコードをオブジェクトへ詰め替える

課題

前後処理は抽象化されたものの、レコードのオブジェクトへの詰め替えは相変わらず煩雑だ。上記コードはあくまで例であるためカラム数も限られているが、実際のプロジェクトでは多くのカラムについて定型的な記述が必要になるだろう。

代表的な Java フレームワーク

このレベルだけに特化したフレームワークは存在しないが、JdbiSpring JdbcTemplate のようなレベル 3 の機能を持つフレームワークは、レベル 2 の機能をあわせて持っている。

また、レベル 1 の「選択基準」で述べた通り、自前でこのレベルのフレームワークを構築することは容易である。Lambda に習熟するためのいい練習台になるはずだ。

選択基準

このレベルの手法を採用すべき場面も多くはない。レベル 1 と同様に、極度に性能を重視する場合や、何らかの事情でフレームワークの使用に制限がかかる場合は選択肢に入る。

また、レコードとオブジェクトの構造が大幅に異なり、手動で柔軟なマッピング処理を書く必要がある場合は、レベル 3 ではなくこのレベルにあえてとどまることもあるだろう。

レベル 3: クエリと単純なオブジェクトのマッピング

レベル 3 では、レベル 2 では手動で対応していたレコードからオブジェクトへの詰め替えを自動化する。以下はレベル 2 と同じ Jdbi の別の API を利用した例だ。

public List<Issue> findByProjectId(long projectId) {
  handle.registerRowMapper(BeanMapper.factory(Issue.class));
  String query = "select id, title, description from issue where project_id = ?";
  List<Issue> issues = handle.createQuery(query).bind(0, projectId)
      .mapTo(Issue.class).list();
  return issues;
}

利用方法

このレベルの手法を利用するために必要なことは以下の通りである。

  • クエリ文字列を指定する
  • クエリパラメータを指定する
  • クエリを実行する

課題

このレベルの手法は一見すると汎用性が高く感じられるかも知れないが、オブジェクトの関連ナビゲーションができないことは重大な欠陥だ。実際のアプリケーションは複数のテーブルで構成されている。例えばこの記事の題材であるシンプルな課題管理アプリケーションであれば、issue テーブル以外に、多対一で関連する project テーブルや、一対多で関連する comment テーブルがあるはずだ。それらのデータに対して、オブジェクト指向的な発想であれば、Issue#getProject()Issue#getComments() のようなメソッドで関連するオブジェクトとしてアクセスできることが自然だ。だが、このレベルの手法ではそうした関連ナビゲーションは実現できない。

このレベルの手法で取得できるのは、単体のオブジェクトか、オブジェクトのリスト (二次元の表構造) だけだ。関連ナビゲーションに相当するデータアクセスを実現しようとする場合は、別々のクエリで取得して自前でマージするロジックを書くか、JOIN したひとつのオブジェクトとして無理やり扱うかのどちらかしかできない。

こうした制約の下では、ドメイン駆動設計のようなリッチなドメインモデルを前提としたアーキテクチャの実現は絶望的だ。結果として、各画面の表示の都合に引きずられた個別のモデルが増殖し、ドメイン中心ではなく画面中心のアプリケーションが出来上がる。このレベルの手法で取得したオブジェクトをリッチなドメインモデルに自前で詰め替える選択肢もなくはないが、そんな面倒なことをするくらいなら素直にレベル 4-5 の手法を学習したほうが多くの場合低コストで済むはずだ。

また、OOUI のようなユーザに自由なインタラクションを提供する UI においては、関連ナビゲーションはほとんど必須の機能である。関連ナビゲーションのできないモデルは結果的に使いやすい UI の実現を阻害する要因となりうる。

代表的な Java フレームワーク

このレベルの代表的なフレームワークとしては、JdbiSpring JdbcTemplate が挙げられる。なお、厳密にはこの両者ともレベル 4 に相当するデータアクセスには頑張れば対応できる (が、を見ればわかる通り、煩雑だ)。また、sql2o や、昔懐かしい Commons DbUtils など、他にも多くの選択肢がある。

また、シンプルな O/R マッピングフレームワークとして一定の支持を集めている Doma も、参照系についてはレベル 3 までにしか対応していない。こちらは頑固にも設計思想として関連ナビゲーションには対応しないことを明言している。

選択基準

UI が定型的なアプリケーションや、データアクセスが単純なバッチなど、このレベルの手法で十分な場面はそれなりにあるはずだ。当初の想定に反して実際の要件は定型的でも単純でもなかった、というありがちな展開にならないことを祈りながら使おう。

レベル 4: クエリと関連ナビゲーション可能なオブジェクトのマッピング

レベル 3 では実現できなかった関連ナビゲーションについて、MyBatis を使った実現例を見てみよう。

まず、issue テーブルと関連する project comment テーブルの定義は以下のようになる。

create table project
(
   id bigint primary key,
   name varchar (100)
);
create table comment
(
   id bigint primary key,
   issue_id bigint,
   description text
);

次に、マッピング対象の Java クラスを以下に示す。

@Data
public class Issue {
    private long id;
    private Project project;
    private List<Comment> comments;
    private String title;
    private String description;
}
@Data
public class Project {
    private long id;
    private String name;
}
@Data
public class Comment {
    private long id;
    private String description;
}

さらに、これらをマッピングする設定を書く。ここでは XML ベースの方式 を使用している (なお MyBatis には他に、アノテーションベースの方式もある)。

<resultMap id="issueResult" type="Issue" autoMapping="true">
  <id property="id" column="id" />
  <association property="project" column="project_id"
    select="Project.find" />
  <collection property="comments" column="id"
    select="Comment.findByIssueId" />
</resultMap>
<select id="findByProjectId" parameterType="long"
  resultMap="issueResult">
  <![CDATA[
    select
      id,
      project_id,
      title,
      description
    from
      issue
    where
      project_id = #{projectId}
  ]]>
</select>
<resultMap id="projectResult" type="Project"
  autoMapping="true">
  <id property="id" column="id" />
</resultMap>
<select id="find" parameterType="long"
  resultMap="projectResult">
  <![CDATA[
    select
      id,
      name
    from
      project
    where
      id = #{id}
  ]]>
</select>
<resultMap id="commentResult" type="Comment"
  autoMapping="true">
  <id property="id" column="id" />
</resultMap>
<select id="findByIssueId" parameterType="long"
  resultMap="commentResult">
  <![CDATA[
    select
      id,
      description
    from
      comment
    where
      issue_id = #{issueId}
  ]]>
</select>

最後に、上記の設定に基づいてデータアクセスを実行する。

public List<Issue> findByProjectId(long projectId) {
  return sqlSession.selectList("Issue.findByProjectId", projectId);
}

利用方法

このレベルの手法を利用するために必要なことは以下の通りである。

  • クエリ文字列を指定する
    • 上記例では XML 設定で指定している
  • クエリ結果とオブジェクトのマッピングを設定する
    • 上記例では XML 設定で指定している
  • クエリパラメータを指定する
  • クエリを実行する

課題

これで関連ナビゲーションは実現できるようになったが、例を見れば明らかな通り、その設定は簡単ではない。さらに参照系クエリの種別が増えたり、更新系の insert/update/delete 処理が必要になったりした場合は、都度手動で SQL を記述する必要がある。後述の通り自動生成による対策は存在するが、効果は限定的だ。

また、定型処理の抽象化についても弱点がある。監査系カラム (登録日時、更新日時、登録ユーザ、更新ユーザ、…) の自動入力や、バージョン番号による楽観的ロックといった、レベル 5 の手法であれば容易に抽象化可能な定型処理について、このレベルの手法では都度手動で SQL を記述しなければならない。

さらに、ここまでのレベル共通の問題として、手動で SQL を記述している以上、特定の DBMS への依存性が発生する。ポータビリティを考慮した標準準拠の SQL だけで全ての要件を満たすことは困難だ。日常的な開発においてこの問題を意識する機会は少ないが、システムリプレースのような大規模改修の話が持ち上がると事態の深刻さが一気に顕在化する。

なお、レベル 5 で大暴れする N + 1 問題は、このレベルでも発生する可能性がある。ただし、このレベルの手法は自分が SQL を書いた通りにしか動かないため、その責任はフレームワークではなく自分自身にあり、また対策の仕方も明確だ。例えば、上述の例では実は N + 1 問題が発生するが、以下のように SQL を JOIN を用いたものに置き換えれば問題は解決する。

<resultMap id="issueResultWithProjectAndComments"
  type="Issue" autoMapping="true">
  <id property="id" column="id" />
  <association property="project" columnPrefix="p_"
    resultMap="Project.projectResult" />
  <collection property="comments" columnPrefix="c_"
    resultMap="Comment.commentResult" />
</resultMap>
<select id="findByProjectIdWithProjectAndComments"
  parameterType="long" resultMap="issueResultWithProjectAndComments">
  <![CDATA[
    select
      i.id as id,
      i.project_id as project_id,
      i.title as title,
      i.description as description,
      p.id as p_id,
      p.name as p_name,
      c.id as c_id,
      c.description as c_description
    from
      issue i
      inner join project p on i.project_id = p.id
      left outer join comment c on i.id = c.issue_id
    where
      project_id = #{projectId}
  ]]>
</select>

付随する機構

このレベルの手法には、以下のような機構が付随する。

  • Lazy Loading
    • 関連ナビゲーションにおいて、Issue#getProject() のようなメソッドが呼ばれた時点で SQL を実行する
    • 反面、N + 1 問題のリスクがある
  • カスタム型変換
    • データベース側のシンプルな型を Java 側のより表現力のある型に変換する
    • 例として MyBatis の TypeHandler を参照
  • 高度な SQL テンプレートエンジン
    • SQL について、条件分岐・ループのような制御構造や、共通部分の括り出しのような抽象化を実現する
    • 例として MyBatis の動的 SQL を参照
  • ソースコードと設定のスキーマからの自動生成
    • データベーススキーマから CRUD 処理に必要なソースコードと設定を自動生成する
    • 生成された成果物を変更するとその後のデータベーススキーマの変更にうまく追従できない (Generation Gap パターンのようなワークアラウンドの効果は限定的である)
    • 例として MyBatis Generator を参照
  • 簡単なキャッシュ
    • あくまで簡単なものであり、レベル 5 で実現できるキャッシュに比べると機能は限定的である
    • 例として MyBatis のキャッシュ を参照

代表的な Java フレームワーク

このレベルの代表的なフレームワークは例にも挙げた MyBatis である。歴史が長いこともあって、「付随する機構」で挙げた機能をフルセットで持っている。

また、レベル 5 で登場する JPA について、その一部である Native Query はレベル 4 の手法と見なせる。ただし、MyBatis と比べると基本機能の使いやすさや「付随する機構」で挙げた機能への対応に差がある。

選択基準

データベースに関するしがらみは、レベル 4 の手法を選ぶ理由になりうる。「しがらみ」とは例えば、変更できないレガシースキーマ、UI 要件に対して齟齬があるデータ構造、既存 SQL の流用要件などである。

また、ドメイン駆動設計的なリッチなドメインモデルを前提とすると、マッピング設定の柔軟性においてレベル 5 より優れるレベル 4 の手法が有力な選択肢になる。

さらに、性能要件とチームメンバのスキルを考慮した上で、レベル 5 のパフォーマンスリスクに対するローリスク・ローリターンな代替選択肢としてレベル 4 の手法を選ぶこともあるだろう。

レベル 5: テーブルとオブジェクトのマッピング

レベル 4 では都度手動で SQL を記述していたが、そもそも一般的なアプリケーションにおいてはテーブルとオブジェクトの構造は多くの点で似通っているため、両者をうまくマッピングできればその部分は省力化できるはずだ。以下、標準規格の JPA に準拠した Hibernate ORM を利用した例を見てみよう。

データベーススキーマはレベル 4 までと同一である。マッピング対象の Java クラスはレベル 4 と同様だが、マッピング設定用のアノテーションが追記されている。

@Data
@Entity
public class Issue {
    @Id
    private long id;
    @ManyToOne
    @JoinColumn(name = "project_id")
    private Project project;
    @OneToMany
    @JoinColumn(name = "issue_id")
    private List<Comment> comments;
    private String title;
    private String description;
}
@Data
@Entity
public class Project {
    @Id
    private long id;
    private String name;
}
@Data
@Entity
public class Comment {
    @Id
    private long id;
    private String description;
}

上記に基づいて、データアクセスを実行する。

public List<Issue> findByProjectId(long projectId) {
  String query = "select i from Issue i where i.project.id = ?1";
  List<Issue> issues = entityManager.createQuery(query, Issue.class)
      .setParameter(1, projectId).getResultList();
  return issues;
}

利用方法

このレベルの手法の利用方法は以下の通りである。

  • テーブルとオブジェクトのマッピングを設定する
    • 上記例ではアノテーションで指定している
    • 他に、XML 設定で指定する方法もある (が、現在ではあまり使われない)
  • クエリ文字列を指定する
    • 上記例では DBMS 依存の SQL ではなく、抽象化された JPQL である
    • 上記例ではクエリ実行時に Java コード内の文字列で指定している
    • 他に、アノテーションで指定する Named Query もある
  • クエリパラメータを指定する
  • クエリを実行する

課題

このレベルの手法の大きな課題は意図しない SQL 発行によるパフォーマンス劣化であり、中でも代表的なものは N + 1 問題である。N + 1 問題とは、主要なテーブルへの 1 回のクエリ結果で返ってきたレコード N 件について、関連するテーブルへのクエリが N 回実行されてしまうことである。この記事の題材である課題管理アプリケーションに基づいて説明するなら、課題一覧画面のデータアクセスにおいて、issue テーブルへの 1 回のクエリが実行された後で、ループ中に Issue#getComments() が都度呼び出されることで、comment テーブルに対して先のクエリで取得した issue レコードの件数に相当する N 回のクエリが実行されるような事態である。

JPA における N + 1 問題への主要な対応策のひとつは FETCH JOIN である。例えば、例えば、上述の例では実は N + 1 問題が発生するが、以下のようにクエリを FETCH JOIN を用いたものに置き換えれば問題の発生は抑止できる。

public List<Issue> findByProjectIdWithProjectAndComments(long projectId) {
  String query = "select distinct i from Issue i join fetch i.project"
      + " left join fetch i.comments where i.project.id = ?1";
  List<Issue> issues = entityManager.createQuery(query, Issue.class)
      .setParameter(1, projectId).getResultList();
  return issues;
}

実際に生成される SQL クエリは以下のようになる。

select
    distinct issue0_.id as id1_1_0_,
    project1_.id as id1_2_1_,
    comments2_.id as id1_0_2_,
    issue0_.description as descript2_1_0_,
    issue0_.project_id as project_4_1_0_,
    issue0_.title as title3_1_0_,
    project1_.name as name2_2_1_,
    comments2_.description as descript2_0_2_,
    comments2_.issue_id as issue_id3_0_0__,
    comments2_.id as id1_0_0__
from
    issue issue0_
inner join
    project project1_
        on issue0_.project_id=project1_.id
left outer join
    comment comments2_
        on issue0_.id=comments2_.issue_id
where
    issue0_.project_id=?

FETCH JOIN 以外にも、@Fetch(FetchMode.SUBSELECT) を利用する方法や、Entity Graph を利用する方法などがある。また、@Where@Filter といった細粒度の関連制御が必要になる場面もある。

パフォーマンスについては上記のような各種対策が存在するが、これらの習得に小さくない初期学習コストがかかることはレベル 5 導入にあたっての大きな課題の一つである。おそらく多くの現場では、そうしたコストは前もって意識的に支払われることはなく、その結果として発生するパフォーマンス問題の責任が漠然とフレームワークに押し付けられているのではないだろうか。なお、Hypersistence Optimizer のような解析ツールが活用できれば、初期学習コストのある程度の低減は期待できると思われる。

また、標準規格である JPA の不備の問題もある。例えば、上述の Entity Graph の挙動には実装依存の部分があり、さらに @Where@Filter に至っては標準化されていない Hibernate 実装依存機能である。

さらに、そもそもの制約として、レベル 5 の前提であるテーブルとオブジェクトの構造類似性が要件的に低いプロジェクトでは効果が期待できない点が挙げられる。こうした場合は自由にクエリが書けるレベル 3-4 の手法のほうが適切である。

付随する機構

このレベルの手法には、以下のような機構が付随する。

  • ライフサイクル管理
    • オブジェクトの変更をフレームワークが検知し、状態に応じた適切な永続化処理を行う
    • プログラマの意図と異なる動作をすることが多く、O/R マッピングに対する悪評の源泉の一つとなっている
    • EBean のようにあえて一元的なライフサイクル管理を機能から外すフレームワークもある
  • 高度なキャッシュ
    • ライフサイクル管理の一次キャッシュに加えて、汎用的なキャッシュライブラリと連携可能な二次キャッシュが利用できる
    • 例として Hibernate のキャッシュを参照
  • タイプセーフクエリ
    • クエリを文字列で書くとコンパイル時に誤りがチェックできないため、代替手段として Java コードでクエリを書く機構を提供する
    • JPA 標準で Criteria API が提供されているが、信じられないくらいに使いにくい
    • 非標準だがより使いやすい拡張手段として QueryDSL がある
    • タイプセーフクエリに特化した特殊なフレームワークとして jOOQ がある
  • Lazy Loading
    • レベル 4 と同様
  • カスタム型変換
  • ソースコードのスキーマからの自動生成
  • ソースコードからスキーマの自動生成
    • 上とは逆に、Java ソースコードとマッピング設定から、データベーススキーマを自動生成する
    • 例として Hibernate の Schema Generation を参照

代表的な Java フレームワーク

このレベルの代表的なフレームワークは標準規格の JPA である。JPA の実装系としては、この記事で扱った Hibernate ORM の他に、EclipseLink もある。どちらの実装系を選ぶかについては、使用するアプリケーションサーバや上位フレームワークでどちらがデフォルトになっているかに従って決めることになるだろう。

また、「付随する機構」で挙げた EBeanjOOQ、さらに Reladomo のような代替選択肢はある。どれも非標準であること、また特に jOOQ と Reladomo についてはかなり癖が強いことを考慮して、選択にあたっては慎重な態度で臨むべきだ。

選択基準

課題でも触れた通り、このレベルの手法を利用するにあたっては初期学習コストやスキーマに関する厳しい前提条件がある。前提条件が満たされれば生産性に関する高いリターンが見込めるが、そうでない場合は工数の浪費要因になりかねない。

また、何らかの制約で標準準拠が強制される場合は、レベル 3-4 を飛ばしてレベル 5 の JPA しか選択肢はない (上述の通りレベル 4 相当の Native Query 機能は使える)。

プロジェクトにとって最適なレベルの手法を選ぶには

さて、どんな場面でどのレベルの手法を選ぶべきか、各レベルの「選択基準」記述とある程度重複してしまうが、改めてざっくり振り返ってみよう。

まず、レベル 1-2 から始めることは少ない。レベル 3 で十分か、もしくはレベル 4-5 が必要か、が多くの場合に最初の判断の分かれ目になる。

プロジェクトの複雑性が小さい場合はレベル 3 の手法で十分だろう。ただし、複雑性に関してわかりやすい単一の指標はなく、UI の特性・データアクセスの特性・案件の規模・チームの人員構成などから総合的に判断することになる。

プロジェクトの複雑性が大きい場合はレベル 4-5 から選ぶことになる。スキーマがレガシーもしくはマッピングの工夫が必要で不確実性の低さを重視したい場合はレベル 4 の手法が、それ以外の場合はレベル 5 の手法がおそらく適切である。

なお、現状のレベル 5 の手法が完成されているかといえばそうではない点が問題をより複雑にしている。初期学習コストの高いライフサイクル管理をオプションにする、関連ナビゲーション制御の仕様をよりシンプルにして標準に組み込む、必要に応じてリッチなレベル 4 の手法と組み合わせられるようにする、などの改善があれば、よりレベル 5 の手法を選ぶべき場面は広がるはずだ。

(余談) どのレベルから「O/R マッピング」なのか

どのレベルから「O/R マッピング」と呼ぶべきなのかは自明ではない。レベル 5 の手法を O/R マッピングと呼ぶことに異論のある向きは存在しないだろう。また、おそらくレベル 4 の手法についても、「広義の」という接頭辞でもつけておけば O/R マッピングと呼ぶことにはほとんど反対はされないと思われる。だが、レベル 3 の手法については、JDBI のように O/R マッピングとは一線を画した存在と称する場合もあれば、かつての Doma のように堂々と O/R マッパーであることを謳う場合もある。さらに、本来レベル 1-2 に属する手法についても、データアクセスコンポーネントのインタフェースはレベル 3-5 と変わらないもの (上から見たら O/R マッピングしているようにしか見えない Repository) を技術的には提供可能である。

どのレベルから「O/R マッピング」なのかについては、以下のような複数の立場があるだろう。どの立場が正しいのかについては、あまり生産的な議論になるとは思えないため深入りしない。

  • 設計手法と見なす立場
    • データアクセスコンポーネントのインタフェースが条件を満たしていれば実装がレベル 1 でも対象に含む
  • 実装技術と見なす立場
    • オブジェクトグラフをマッピング可能なフレームワークとしてのレベル 4-5 のみを対象に含む
    • さらに、ネイティブな SQL を隠蔽可能なレベル 5 のみに限定する

おわりに

以上、O/R マッピングについて、レベルを 5 段階に分けて、それぞれ固有の必然性と使い分け基準があることを示した。この記事によって、O/R マッピングに対する誤解や、開発現場での要素技術選定における不幸なミスマッチが、少しでも減ることを願う。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
この記事は以下の記事からリンクされています
ts7i鬼滅の概念モデリングからリンク

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
BitriseでCI/CDを実現したモバイルアプリ開発の成果を共有しよう!
~
祝 Azure Static Web Apps GA!Static Web Apps のあなたの「いち推し」ポイント、教えてください
~