↩️

AI時代にORMなんて必要なんですかね?

に公開1

新規で構築するシステムの設計を考えていて、 「今の時代にORMなんているんか???」 という思いに至ったので、これを書いてます。

ORMなしでAIにDBアクセスコードを生成する

AIでコードを生成する前提として、

  • AIは生SQLを書くのが得意
  • オブジェクトマッピングみたいなボイラープレートをAIに生成させるコストは極小(人間が手で書くとめちゃくちゃ時間がかかる)

という点が挙げられます。

そのため、AIを使う前提であれば、ORMなしで以下の作業を行っても、必要なコスト(特に時間)は極小です。

  • ドメイン要件を伝えてSQLを生成させる
  • オブジェクトマッピング処理(いわゆるDAO)を生成させる
  • 単体テストコードを生成させる

というか、ドメインロジックを書いていく過程で上記のようなDBアクセスコードを、都度必要となった分だけ生成させていくのであれば、この部分の生成に時間がかかってると認識することはないんじゃないかと思います。

もはやORMを使う方が弊害が大きいのでは

ORMを使うと、以下のような弊害があります。

  • 実行時にオーバーヘッドがかかる
  • クエリビルダーとかいう「ORM固有で機能不足なDSL」をSQLとは別に覚えないといけない
    • left outer joinとかサブクエリーとかって、クエリービルダーでどう書くんだっけ?
    • 結局SQLを書いた方が早くね?
  • RDBごとの方言がORMとかみ合わない
  • ORMの都合に合わせてマイグレーションスクリプトを組まないといけない

ORMが「SQLであればできること」のすべてをカバーできない問題は、20年以上あるORMの歴史でずっと解決してきていません。そもそも解決することは難しそうです。

実行時オーバーヘッドについては大した問題ではないですが、オーバーヘッドがないに越したことはありません。マイグレーションについても、ORMに縛られないツールが沢山あるので、ORMから分離されている方が嬉しいです。

ORMなしで開発しても大したコストがかからない一方で、使う方ことで弊害があるんだったら、もう使うのやめたらよくない?という発想になるのは自然かと思います。

ORMが出てきた背景(ここは蛇足なので読まなくていいです)

そもそもORMが世に出てきた経緯は 「DAO/DTOを手で書くのがめんどくせえ」 からだと認識しています。

概ね2005年より前には日本国内でもORMは一般化しており、特にJavaや.NET Frameworkである程度の開発規模のあるプロジェクトでは、使うのが当たり前になっていたと記憶しています。この時代で特に有名なプロダクトは、Hibernate、iBATIS、S2Daoあたりでした。

この時代の感じで、JavaでORMを使わずにDAO/DTOを書くと、だいたいこんな感じのコードになります。コードが長いんだよっていうのだけが必要な情報で、中身を読む必要はないです。(AIに生成させた例示なので、構文が誤っている可能性はあります。)

package example.dto;

import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private long id;
    private String name;
    private String email;
    private String status;
    private Date createdAt;

    public User() {}

    public User(long id, String name, String email, String status, Date createdAt) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.status = status;
        this.createdAt = createdAt;
    }

    public long getId() { return id; }
    public void setId(long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }

    public Date getCreatedAt() { return createdAt; }
    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }

    @Override
    public String toString() {
        // 省略
    }
}
package example.dao;

import example.dto.User;

import java.sql.SQLException;
import java.util.List;

public interface UserDao {
    long create(User user) throws SQLException;
    User findById(long id) throws SQLException;
    List<User> findAll(int offset, int limit) throws SQLException;
    boolean update(User user) throws SQLException;
    boolean deleteById(long id) throws SQLException;
}
package example.dao;

import example.dto.User;
import example.jdbc.ConnectionFactory;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class UserDaoImpl implements UserDao {
    private final ConnectionFactory connectionFactory;

    public UserDaoImpl(ConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }

    @Override
    public long create(User user) throws SQLException {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        final String sql = "INSERT INTO users (name, email, status) VALUES (?, ?, ?) RETURNING id";

        try {
            conn = connectionFactory.getConnection();
            ps = conn.prepareStatement(sql);
            ps.setString(1, user.getName());
            ps.setString(2, user.getEmail());
            ps.setString(3, user.getStatus() != null ? user.getStatus() : "ACTIVE");

            rs = ps.executeQuery();
            if (!rs.next()) {
                throw new SQLException("INSERT succeeded but no id returned (RETURNING failed)");
            }
            long id = rs.getLong(1);
            if (rs.wasNull()) {
                throw new SQLException("Returned id is NULL (unexpected)");
            }
            user.setId(id); //副作用があるけど、当時はこれが普通だった記憶がある
            return id;
        } finally {
            if (rs != null) try { rs.close(); } catch (SQLException ignore) {}
            if (stmt != null) try { stmt.close(); } catch (SQLException ignore) {}
            if (conn != null) try { conn.close(); } catch (SQLException ignore) {}
        }
    }

    @Override
    public User findById(long id) throws SQLException {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        final String sql = "SELECT id, name, email, status, created_at FROM users WHERE id = ?";

        try {
            conn = connectionFactory.getConnection();
            ps = conn.prepareStatement(sql);
            ps.setLong(1, id);
            rs = ps.executeQuery();

            if (rs.next()) return mapRow(rs);
            return null;
        } finally {
            if (rs != null) try { rs.close(); } catch (SQLException ignore) {}
            if (stmt != null) try { stmt.close(); } catch (SQLException ignore) {}
            if (conn != null) try { conn.close(); } catch (SQLException ignore) {}
        }
    }

    @Override
    public List<User> findAll(int offset, int limit) throws SQLException {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        // StringBuilderを使えって怒られるやつ
        final String sql =
                "SELECT id, name, email, status, created_at " +
                "FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?";

        try {
            conn = connectionFactory.getConnection();
            ps = conn.prepareStatement(sql);
            ps.setInt(1, limit);
            ps.setInt(2, offset);
            rs = ps.executeQuery();

            List<User> results = new ArrayList<User>();
            while (rs.next()) results.add(mapRow(rs));
            return results;
        } finally {
            if (rs != null) try { rs.close(); } catch (SQLException ignore) {}
            if (stmt != null) try { stmt.close(); } catch (SQLException ignore) {}
            if (conn != null) try { conn.close(); } catch (SQLException ignore) {}
        }
    }

    @Override
    public boolean update(User user) throws SQLException {
        // 省略
    }

    @Override
    public boolean deleteById(long id) throws SQLException {
        // 省略
    }

    private User mapRow(ResultSet rs) throws SQLException {
        User u = new User();
        u.setId(rs.getLong("id"));
        u.setName(rs.getString("name"));
        u.setEmail(rs.getString("email"));
        u.setStatus(rs.getString("status"));

        Timestamp ts = rs.getTimestamp("created_at");
        u.setCreatedAt(ts != null ? new java.util.Date(ts.getTime()) : null);
        return u;
    }
}

これを全部手で書くみたいのはやってられないよね、というのは伝わると思います。そりゃあORMが欲しくなります。

蛇足ですが、当時はこれを入力補完もないテキストエディタ(秀丸とかサクラエディタ)で書いてる人たちも沢山居ました…。(EclipseなどのIDEもありましたが、しょぼい開発PCしかなくてIDEを使うよりテキストエディタの方が早いみたいな話も…。)

Discussion

Katsuyuki KarasawaKatsuyuki Karasawa

ORMは要らないが、AIに対するガードレールは一定必要...という考えの人間なので、クエリビルダとか、sqlcみたいな薄いORMを使ってます。