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-trip | batch 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=TRACEp6spyを使用 した監視 :
<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