みなさんこんにちは( ;∀;)
普段、Spring Bootでサーバーサイドの開発を行うことも多いのですが、DBへのCrud操作のコードをRepository層に記述する際、ORMを利用していました。ORMを使うときにはSpring Data JPA(Java Persistence API)というチョー便利なモジュールを使っているのです。
ちなみにSpring Data JPAとは、リレーショナルデータベースへのアクセスと操作を簡単にするためのモジュールでございます。
データベース層の実装を簡単にしてくれる開発者にとっては素晴らしいものなのですが、このようなORMを使っている際にふと思いました。
「じゃあ、生のSQLを使う場面てどこ?」
生のSQLが存在するということは、存在意義があるから!
そう思い、SQLとORMの比較を実際に実装してみた情報をもとに解説していきたいと思います!
ORMの概要
ORM(Object-Relational Mapping)について説明します。ORMは、プログラム内のオブジェクトとRDBのテーブル間のマッピングを自動的に行う技術です。すなわち開発者は生のSQLクエリを直接記述することなく、DB操作をオブジェクト指向の言語で行うことができるのです。
ORMのメリット
ORMを利用することで、データベースのテーブルとオブジェクト間のマッピングを自動的にしてくれる。なので、開発時間が短縮される。ORMを使用すると、DB操作をオブジェクト指向のコードで表現できる。これによって、DBの操作が直感的に理解しやすくなって、コードの可読性が向上する。開発初心者でSQL文を書けない人にはお勧めできるかも。
例えば、ユーザーの名前が「hogehoge」のユーザー情報を検索する場合は、以下のようなSQLを使用します。
SELECT * FROM users WHERE name = 'hogehoge';
次にSpringDataJpaを利用すると以下のようになります。
public interface UserRepository extends JpaRepository<User, Long> {
User findByName(String name);
}
SQLでは、「SELECT * FROM users WHERE name = 'hogehoge';」と書くところを、Spring Bootでは「User findByName(String name);」と割と直感的に(個人差はあると思いますが、、)ユーザー情報を検索できますね。
ORMと生のSQLを比較
では、本題に戻ってSQLの存在意義、すなわちSQLとORMを比較したときにこういう場合はSQLを利用したほうがいいんじゃないか!と感じた点をご説明します。
複雑なクエリを扱うとき
では、複数のテーブルからデータを結合して、特定の条件で検索する場合を考えてみましょう。
- ケース:ordersテーブル・productsテーブル・categoriesテーブルの3つのテーブルを結合して、各カテゴリの総売り上げを計算して、最も売り上げが高い製品カテゴリを求める場合
上記のケースを満たす生のSQLは以下のようになります。
SELECT
c.category_name,
SUM(o.quantity * o.price) AS total_sales
FROM
orders o
INNER JOIN products p ON o.product_id = p.id
INNER JOIN categories c ON p.category_id = c.id
GROUP BY
c.category_name
ORDER BY
total_sales DESC
LIMIT 1;
次にSpring Bootでのクエリのコードを書いてみます。
リポジトリ層でのコードは以下のようになります。
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT new com.example.dto.CategorySalesDTO(c.categoryName, SUM(o.quantity * o.price)) " +
"FROM Order o " +
"JOIN o.product p " +
"JOIN p.category c " +
"GROUP BY c.categoryName " +
"ORDER BY SUM(o.quantity * o.price) DESC")
Page<CategorySalesDTO> findTopCategoryByTotalSales(Pageable pageable);
}
上記のメソッドにより、CategorySalesDTO
のPage
オブジェクトを返します。
次にクエリの結果を保持するDTOのコードは以下のようになります。
public class CategorySalesDTO {
private String categoryName;
private BigDecimal totalSales;
public CategorySalesDTO(String categoryName, BigDecimal totalSales) {
this.categoryName = categoryName;
this.totalSales = totalSales;
}
// getters and setters
}
そしてService層のクラスでは、SQLのLIMIT 1に相当するコードを書くために、以下のようなコードを書きます。
public CategorySalesDTO getTopCategoryByTotalSales() {
Pageable topOne = PageRequest.of(0, 1);
Page<CategorySalesDTO> result = orderRepository.findTopCategoryByTotalSales(topOne);
return result.getContent().get(0); // 最上位の結果を返す
}
どうでしょうか?
生のsqlを書く時と比べて、Repository層・DTO・Service層と3つのファイルにコードを書く必要があります。
こう考えると、特定の複雑なクエリに関しては、SQLのほうが簡潔に書けることがわかりますね。
また,ORMと生のSQLの比較でよく出されるN+1問題についても少し調べてみました.
N+1問題
N+1問題は,SQLが大量増殖してしまう問題のことです.ORMを使用することで,データベースのテーブルをオブジェクトとして扱うことができ,SQLクエリを直接書くことなくデータベースの操作が可能になりますが,この抽象化により,データベースとのやり取りが不透明になることがあり,特にN+1問題が発生しやすくなります.
例えば,下記のようなテーブルが存在するとします.
すべてのSchoolとそれに関するStudentを取得するシンプルな操作を考えたときに,Spring BootのORMは以下のようになります.
@Service
public class ShopService {
@Autowired
private SchoolRepository schoolRepository;
public void printSchoolsAndStudents() {
List<School> schools = schoolRepository.findAll();
for (School school : schools) {
System.out.println(school.getName());
for (Student student : school.getStudents()) {
System.out.println(" - " + student.getName());
}
}
}
}
上記のコードでは,最初にすべてのSchoolを取得するための1回のクエリが発生する.しかし,各Schoolのstudentにアクセスすると,そのSchoolに関連するStudentをロードするための追加のクエリが発生する.もし,100個のSchoolが存在する場合,Studentを取得するためにさらに100回のクエリが発生して,合計101回のクエリが実行されることになる.これがN+1問題です.
生のSQLを使用する場合,N+1問題を回避するために,関連するデータを1度のクエリで取得することができます.
SELECT s.name AS shopName, m.name AS menuName
FROM Shop s
LEFT JOIN Menu m ON s.id = m.shop_id
ORDER BY s.id, m.id;
生のSQLを使用してデータを効率的に取得することで,データベースへのクエリ回数を削減し,アプリケーションのパフォーマンスを向上させることができます.
まとめ
今回はクエリをたたくときに、生のSQLを書く場合とORMを書く場合の比較をSpring Bootの具体的なコードを用いて説明しました。
まだ未熟なエンジニアである私が得たこととしては、
- 複雑なクエリを扱う場合は生のSQLを使用するほうが良い!
- 一般的なデータのアクセスに関してはORMを利用する。($・・)/~~~
という結論に至りました。
エンジニア歴の長い人からしたら、突っ込みが来そうな内容ではありましたが、クエリを今後も書いていく中で「こういうケースは生のSQLを書いたほうが良いな」とか、そういった知見を学んでいけたらと思います。
今回は、私の記事をお読みいただきありがとうございました($・・)/~~~