ORM Anti-Patterns

一部のコンテンツは LLM によって生成されており、まだ手動で検証されていません。

ORM(Object-Relational Mapping)使用(しよう) における一般的(いっぱんてき) なアンチパターンとベストプラクティス。

  flowchart TB
    subgraph N1["N+1 問題"]
        Q1[Query 1: findAll Orders]
        Q2[Query 2: getCustomer #1]
        Q3[Query 3: getCustomer #2]
        Q4[Query N+1: getCustomer #N]
        Q1 --> Q2
        Q1 --> Q3
        Q1 --> Q4
    end
    subgraph Fix["JOIN FETCH 解決策"]
        QF[Single Query: Orders + Customers]
    end

クエリ問題

アンチパターン問題(もんだい)ベストプラクティス
N+1 クエリN(けん) のデータ()()()逐次(ちくじ) 関連(かんれん) データを()()JOIN FETCHまたはeager loadingを使用(しよう)
過度(かど) なフェッチSELECT *で不要(ふよう)(れつ)()()projectionを使用(しよう)必要(ひつよう)(れつ) のみ選択(せんたく)
動的(どうてき) 入れ子(いれこ) サブクエリORMが複雑(ふくざつ)非効率(ひこうりつ) なSQLを生成(せいせい)生成(せいせい) されたSQLを確認(かくにん)必要(ひつよう)(おう) じてネイティブクエリを使用(しよう)
Lazy Load多重(たじゅう) クエリループ(ない)複数回(ふくすうかい) lazy loadingが発生(はっせい)事前(じぜん) fetchまたはbatch fetchingを使用(しよう)
N+1 問題の詳細

アンチパターン:

// 1回のクエリで全注文を取得
List<Order> orders = orderRepository.findAll();

// N回のクエリで各注文の顧客を取得
for (Order order : orders) {
    Customer customer = order.getCustomer(); // lazy loadをトリガー
    System.out.println(customer.getName());
}

生成(せいせい) されるSQL:

SELECT * FROM orders;                    -- 1回
SELECT * FROM customers WHERE id = 1;    -- N回
SELECT * FROM customers WHERE id = 2;
SELECT * FROM customers WHERE id = 3;
...

ベストプラクティス(JOIN FETCH):

@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomer();

生成(せいせい) されるSQL:

SELECT o.*, c.* FROM orders o
JOIN customers c ON o.customer_id = c.id;  -- 1回

ベストプラクティス(Entity Graph):

@EntityGraph(attributePaths = {"customer", "items"})
List<Order> findAll();
過度なフェッチ (Over-fetching)

アンチパターン:

// Entityに20列あるが、必要なのは2列のみ
List<User> users = userRepository.findAll();
users.forEach(u -> System.out.println(u.getName()));

ベストプラクティス(Projectionを使用(しよう) ):

// Interface-based projection
public interface UserNameProjection {
    String getName();
    String getEmail();
}

List<UserNameProjection> findAllProjectedBy();

// DTO projection
@Query("SELECT new com.example.UserDTO(u.name, u.email) FROM User u")
List<UserDTO> findAllUserDTOs();
Lazy Loadingの罠

アンチパターン(ループ(ない) でトリガー):

List<Department> departments = deptRepository.findAll();
for (Department dept : departments) {
    // 各イテレーションでクエリが発生
    int employeeCount = dept.getEmployees().size();
}

ベストプラクティス(Batch Fetching):

// Hibernate設定
@BatchSize(size = 25)
@OneToMany(mappedBy = "department")
private List<Employee> employees;

// またはapplication.propertiesで
spring.jpa.properties.hibernate.default_batch_fetch_size=25

ベストプラクティス(サブクエリFetch):

@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "department")
private List<Employee> employees;

バッチ操作問題

アンチパターン問題(もんだい)ベストプラクティス
逐次(ちくじ) INSERT/UPDATE(かく) データで1(かい) のround-tripbatch insert/updateを使用(しよう)
過度(かど) なテーブルJOINクエリの複雑(ふくざつ) さが(たか) い、最適化(さいてきか) 困難(こんなん)クエリを分割(ぶんかつ) またはネイティブSQLを使用(しよう)
インデックスの不活用(ふかつよう)クエリがデータベースインデックスを活用(かつよう) していないクエリ条件(じょうけん) がインデックス(れつ)対応(たいおう) していることを確認(かくにん)
非効率(ひこうりつ) なソート/ページング(おお) きいテーブルでOFFSETページングの性能(せいのう)(わる)keyset paginationを使用(しよう)
バッチ操作の最適化

アンチパターン(逐次(ちくじ) 保存(ほぞん) ):

for (User user : users) {
    userRepository.save(user);  // 毎回1つのINSERT
}

ベストプラクティス(バッチ挿入(そうにゅう) ):

// application.properties
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

// バッチ保存
@Transactional
public void saveAll(List<User> users) {
    for (int i = 0; i < users.size(); i++) {
        entityManager.persist(users.get(i));
        if (i % 50 == 0) {
            entityManager.flush();
            entityManager.clear();
        }
    }
}

ベストプラクティス(JDBC batch):

jdbcTemplate.batchUpdate(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    users,
    50,
    (ps, user) -> {
        ps.setString(1, user.getName());
        ps.setString(2, user.getEmail());
    }
);
ページング性能問題

アンチパターン(OFFSETページング):

// 10000ページ目、前の999900件をスキップ
Pageable pageable = PageRequest.of(10000, 100);
Page<User> users = userRepository.findAll(pageable);

生成(せいせい) されるSQL:

SELECT * FROM users ORDER BY id LIMIT 100 OFFSET 999900;
-- データベースは前の999900件をスキャンしてから返す必要がある

ベストプラクティス(Keyset Pagination):

@Query("SELECT u FROM User u WHERE u.id > :lastId ORDER BY u.id")
List<User> findNextPage(@Param("lastId") Long lastId, Pageable pageable);

生成(せいせい) されるSQL:

SELECT * FROM users WHERE id > 999900 ORDER BY id LIMIT 100;
-- インデックスを使用して直接位置を特定、性能が安定
クエリ計画の確認

SQLロギングを有効(ゆうこう) にしてORMが生成(せいせい) するクエリを確認(かくにん)

Spring Boot / Hibernate:

# application.properties
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

p6spyを使用(しよう) した監視(かんし)

<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version>
</dependency>
spring.datasource.url=jdbc:p6spy:postgresql://localhost/db
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver