先日、同僚からのある質問をきっかけにSpring Data JPAのQuery by Example機能を知ったので、その内容をまとめておきます。
Query by Exampleが解決する問題
問題の発端は、「9つの検索対象カラムがあるテーブルからデータを検索したい」という質問でした。話を聞くと、ユーザーは9つのフィールドのうち、最低一つだけ指定すれば、どのような組み合わせでも入力できるという要件でした。つまり
- 1つのフィールドだけ
- 9つのフィールド全て
- 任意のいくつかのフィールド(組み合わせ自由)
という感じで多くのパターンが考えられます(71通り)。
初めは
「9つのフィールド全てを使って検索するメソッドを一つ定義したらどうか」
と思ったのですが、nullのパラメータがある場合、「このカラムがnullであること」を条件に検索しようとするので使えないことがわかりました。
「ならば72通りのメソッドを用意するしかないのか」
同時に
「ユーザーの入力値をチェックして72通りのif-else文で分岐する」
という悪夢が頭をよぎります。71通りならば気合いで対応できそうなものですが、フィールドの数が増えたり、似たような要件があるたびに同じようなコードを書くのはいいアイディアには思えません。
しばらく調べていると、Spring Data JPAのQuery by Example機能は、まさにこんな問題を解決するための機能で、Query by Example機能を使えばコードやクエリを一行たりとも書くことなくこの要件に対応できることがわかりました。
Query by Exampleの使用例
冒頭の同僚とのやり取りを具体的にするために、ここではシンプルなケースを考えてみましょう。
EMPLOYEEテーブル
ID | name | location | department |
1 | Taro | Fukuoka | Sales |
2 | Hanako | Tokyo | Sales |
3 | Jiro | Tokyo | HR |
このテーブルのクラスは以下の通りです。
@Entity
public class Employee {
private String name;
private String location;
private String department;
}
ここで、従業員を検索する画面を考えます。検索画面では任意のフィールドに検索条件を入力して検索ができます。
- name
- name + location
- name + department
- name + location + department
- location
- location + department
- department
これだけで7通りのパターンになります。
この全てのパターンごとにメソッドを定義して、入力画面からnullチェックをして呼び出すメソッドを分岐させるのは面倒ですし、コードが汚くなります。
こんなときにSpring Data JPAのQuery by Exampleが役立ちます。
Query by Exampleを使う
Query by Exampleは、JpaRepositoryを継承したインターフェイスならば自動的に利用できます。
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
CrudRepositoryでは利用できないので注意しましょう。
JpaRepositoryを継承したレポジトリインターフェイスには以下のメソッドが提供されます。
注目するべきは、Example<S>を受け取るメソッドです。これらのメソッドに、検索条件を指定したオブジェクトをExampleという型でラップして渡すことで、動的に検索条件を指定できます。
また、findAllメソッドはExampleだけでなく、SortやPageableオブジェクトも渡して、ソートやページ分割をすることもできます。
例えば
Employee employee = new Employee();
employee.setLocation("Tokyo");
employeeRepository.findAll(Example.of(employee));
とすれば、locationカラムがTokyoのレコードを検索できます。SQLでいうならば、以下のような感じになります。
SQL
select * from employee where location = 'Tokyo';
フィールドを変えて
Employee employee = new Employee();
employee.setDepartment("Sales");
employeeRepository.findAll(Example.of(employee));
とすれば、departmentカラムがSalesのレコードを検索できます。
SQL
select * from employee where department = 'Sales';
複数のカラムを検索することもできます。
Employee employee = new Employee()
employee.setLocation("Tokyo");
employee.setDepartment("Sales");
employeeRepository.findAll(Example.of(employee));
とすれば、locationがTokyoで、departmentカラムがSalesのレコードを検索できます。
SQL
select * from employee where location = 'Tokyo' and department = 'Sales';
これにより、検索条件の組み合わせが増えたとしてもメソッドや分岐条件を作らなくてもよくなります。
検索条件をさらに指定するExampleMatcher
ExampleMatcherを使うとさらに細かく条件を指定できます。
先ほどの例にあった、locationとdepartmentから検索をするコードですが、いずれかのフィールドが一致したレコードを検索したい場合、以下のようにExampleMatcherを使います。
Employee employee = new Employee();
employee.setLocation("Tokyo");
employee.setDepartment("Sales");
employeeRepository.findAll(Example.of(employee, ExampleMatcher.matchingAny()));
とすれば、locationがTokyoまたはdepartmentカラムがSalesのレコードを検索できます。
SQL
select * from employee where location = 'Tokyo' OR department = 'Sales';
また、特定のフィールドを除外して検索をすることもできます。例えば、departmentフィールドを除外したい場合、以下のようにします。
Employee employee = new Employee();
employee.setLocation("Tokyo");
employee.setDepartment("Sales");
ExampleMatcher matcher = ExampleMatcher.matching().withIgnorePaths("department");
employeeRepository.findAll(Example.of(employee, matcher));
SQL
select * from employee where location = 'Tokyo';
ExampleMatcherでは開始文字列や正規表現、nullの扱いなど、さらに細かく条件を指定できるので、詳細についてはドキュメントを参照してください。
トラブルシューティング
意図した通りの検索結果が得られない場合、ログレベルを上げて、実際に生成されたSQLを確認すると問題解決につながることがあります。
org.hibernateとorg.springframework.dataのログレベルをDEBUGにあげると、SQLを含む詳細なログを出力されます。自動生成されたSQL文のWHERE句以下を見れば、クエリが意図したとおりに動作しない原因がわかるかもしれません。
Example by Queryの詳細
さらに詳しい情報については、以下の公式ドキュメントを参照ください。僕はリファレンスはなんども見たのですが、この部分は何気なく飛ばしていました…せっかく安定感抜群のSpringが実装してくれた機能ですから活用しない手はないですね。
ドキュメント: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example