QueryBuilderのExprクラスでandXとorXを使ってみる
備忘録です。
QueryBuilderのexpr
というメソッドでExprクラスのインスタンスを作ることができます。
$expr = $this->getEntityManager()
->getRepository(Product::class)
->createQueryBuilder('p')
->expr();
こんな感じ。
これで作ったExprクラスのインスタンスには、SQLの=
やlike
に対応したメソッドがたくさん用意されています。
今回は、その中のandX
メソッドとorX
を使った検索機能を作っていきます。
andX、orXとは?
andX
メソッドは、SQLの○○ where ○○ and ○○;
のand
を表現するものです。引数に条件式を与えるか、インスタンスからadd
メソッドを使って条件式を追加することで利用します。
orX
は、同様にSQLのor
を表しています。こちらも、引数に条件式を与えるか、インスタンスからadd
メソッドを使って条件式を追加することで利用することができます。
$andX = $this->getEntityManager()
->getRepository(Product::class)
->createQueryBuilder('p')
->expr()
->andX();
$andX->add($qb->expr()->eq('p.id', ':id'));
または、
$andX = $this->getEntityManager()
->getRepository(Product::class)
->createQueryBuilder('p')
->expr()
->andX($qb->expr()->eq('p.id', ':id'));
$orX = $this->getEntityManager()
->getRepository(Product::class)
->createQueryBuilder('p')
->expr()
->orX();
$orX->add($qb->expr()->eq('p.id', ':id'));
または、
$orX = $this->getEntityManager()
->getRepository(Product::class)
->createQueryBuilder('p')
->expr()
->orX($qb->expr()->eq('p.id', ':id'));
これらの存在を知るまではSQLやDQLを直書きしていました。
正直めっちゃ見た目は悪かったし、タイポしても気付けないことが多くてモヤだったんですが、andX
とorX
を知ってから実際に使ってみて感激しました。
まず見た目がいい。SQL/DQL直書きと比べて遥かに見やすいです。
そして、エラーが分かりやすいし、タイポもしにくくなります。SQLだと基本的にSyntax error
としか出ないですが、Exprクラスのメソッドを使うと何処がエラーの原因になっているのかすぐにわかるエラーメッセージを出してくれるので助かりますね。
動作環境・前提
バージョン | |
---|---|
Mac | 12.x |
PHP | 7.4.33 |
MySQL | 5.7 |
Symfony | 5.4.21 |
作ってみる
では、早速作っていきます。
今回作るのは、商品の新規作成と検索を行う簡単な管理画面っぽいものです。
Controllerとテーブルの作成
Symfonyのコマンドを使って、ControllerとEntityの作成、マイグレーションの実行を行います。
- Controllerの作成
$ php bin/console make:controller
実行するとControllerの名前を求められるので、適当に入力してください。
以降、ここでProductController
というControllerを作成したものとして話を進めていきます。
- Entityの作成
$ php bin/console make:entity
実行すると、Entityの名前とどんなプロパティが必要か聞かれます。こちらも適当に入力してください。
以降、ここでProduct
というEntityを作成したものとして話を進めていきます。また、このエンティティは次のプロパティを持っているとします。
mysql> show columns from product;
+-------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| price | int(11) | NO | | NULL | |
+-------+--------------+------+-----+---------+----------------+
- マイグレーションの実行
$ php bin/console make:migration
$ php bin/console doctrine:migrate
これで、DBにProduct
という名前のテーブルが作成されました。
ターミナルでSQLを叩くと、テーブルが作成されていることが確認できます。
mysql> show tables;
+---------------------+
| Tables_in_tech_blog |
+---------------------+
| messenger_messages |
| product |
+---------------------+
商品一覧
まずは作成済みの商品情報を表示する一覧画面を作っていきます。
この画面に、商品検索用のフォームも作ることにします。
Controllerはこんな感じ。
/**
* @Route("/product", name="product_index", methods={"get", "post"})
*/
public function index(Request $request): Response
{
$form = $this->createFormBuilder()
->add('multi', TextType::class, [
'label' => "Product Id or Name",
'required' => false,
])
->add('price', MoneyType::class, [
'currency' => 'JPY',
'label' => "Product Price",
'required' => false,
])
->add('submit', SubmitType::class)
->getForm()
;
// 以下に検索処理を書く
if ("POST" === $request->getMethod()) {
}
return $this->render('product/index.html.twig', [
'form' => $form->createView(),
]);
}
createFormBuilder
メソッドでフォームを作成しています。
multi
としているフィールドでは、商品IDと商品名のどちらを入力しても検索できるようにします。price
では、商品の料金で検索します。
まだ検索を行うRepositoryのメソッドを作成していないので、if文の中身は空っぽです。
twigはこんな感じ。
{% block body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Product Index</h1>
{# 検索フォーム #}
<form action="{{url('product_index')}}" method="post">
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
</form>
<a href="{{url('product_index')}}"><p>Reset</p></a>
{# この下に作成済みの商品情報を表示する #}
</div>
{% endblock %}
Controllerから渡されたフォームを表示しています。
aタグで作っているのは、このページ自身へのリンクです。ページにアクセスし直すことで検索状況をリセットすることが目的なので、あってもなくてもいいです。
ここまでできたら、ブラウザで確認してみます。
以下のようになっていればOKです。
商品作成
次に、商品を作成するページを作成します。
商品登録を行うRepositoryのメソッドは、先述した方法でエンティティを作成していれば自動で作られているはずです。ProductRepository.php
のadd
というメソッドです。
public function add(Product $entity): void
{
$this->getEntityManager()->persist($entity);
$this->getEntityManager()->flush();
}
このメソッドを利用して商品登録を行う画面を作っていきます。
Controllerはこんな感じ。
/**
* @Route("/product/new", name="product_new", methods={"get", "post"})
*/
public function new(Request $request): Response
{
$form = $this->createFormBuilder(new Product())
->add('name', TextType::class, [
'required' => true,
'label' => "Product Name",
])
->add('price', MoneyType::class, [
'currency' => 'JPY',
'label' => "Product Price",
'required' => true,
])
->add('submit', SubmitType::class)
->getForm()
;
if ("POST" === $request->getMethod()) {
$params = $request->get('form');
$newProduct = new Product();
$newProduct->setName((string)$params['name']);
$newProduct->setPrice((int)$params['price']);
$productRepository = new ProductRepository($this->managerRegistry);
try {
$productRepository->add($newProduct);
} catch (Throwable $e) {
return $this->render('product/new.html.twig', [
'form' => $form->createView(),
'error_message' => "ERROR: {$e->getMessage()}",
]);
}
return $this->redirectToRoute("product_index");
}
return $this->render('product/new.html.twig', [
'form' => $form->createView(),
]);
}
長いのでパーツごとに見ていきます。
$form = $this->createFormBuilder(new Product())
->add('name', TextType::class, [
'required' => true,
'label' => "Product Name",
])
->add('price', MoneyType::class, [
'currency' => 'JPY',
'label' => "Product Price",
'required' => true,
])
->add('submit', SubmitType::class)
->getForm()
;
ここは商品一覧の時と同様に、createFormBuilderでフォームを作成している部分です。
このページでは商品作成を行うので、登録したい商品情報を入力するフォームですね。
プロパティはid
、name
、price
の3つですが、id
は自動で採番されるのでわざわざ指定する必要はありません。
なので、作成するフォールドはname
、price
の2つで十分です。
if ("POST" === $request->getMethod()) {
$params = $request->get('form');
$newProduct = new Product();
$newProduct->setName((string)$params['name']);
$newProduct->setPrice((int)$params['price']);
$productRepository = new ProductRepository($this->managerRegistry);
try {
$productRepository->add($newProduct);
} catch (Throwable $e) {
return $this->render('product/new.html.twig', [
'form' => $form->createView(),
'error_message' => "ERROR: {$e->getMessage()}",
]);
}
return $this->redirectToRoute("product_index");
}
ここは商品登録を行う部分です。先述した通り、ProductRepository.php
のadd
メソッドを使っています。
try-catch
を使って、商品登録に失敗した場合はエラーメッセージを返すようにしています。
商品登録に成功したら、商品一覧にリダイレクトします。
twigはこんな感じ。
{% block body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Create Product</h1>
{# 商品作成フォーム #}
<form action="{{url('product_new')}}" method="post">
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
</form>
<a href="{{url('product_index')}}"><p>Back to index</p></a>
</div>
{% endblock %}
商品一覧のtwigとほぼ変わりませんね。商品作成用のフォームと、商品一覧へのリンクがあるだけです。
ここまでできたら、商品の新規作成ができるようになっているはずです。
ターミナルでSQLを叩くと、商品がDBに登録されているのが確認できます。
mysql> select * from product;
+----+-----------+-------+
| id | name | price |
+----+-----------+-------+
| 1 | Product A | 1000 |
| 2 | Product B | 2000 |
| 3 | Product C | 3000 |
+----+-----------+-------+
商品検索
さて、いよいよ本題の検索機能に取り掛かります。
まずはRepositoryに検索を行うメソッドを実装していきましょう。
/**
* @param int|string $multi
* @param int|string $price
* @return Product
*/
public function findMany($multi = null, $price = null)
{
$rep = $this->getEntityManager()->getRepository(Product::class);
$qb = $rep->createQueryBuilder('p');
$andX = $qb->expr()->andX();
if (!is_null($multi)) {
$andX->add(
$qb->expr()->orX(
$qb->expr()->eq('p.id', ':id'),
$qb->expr()->like('p.name', ':name')
)
);
$qb->setParameter(':id', (int)$multi)
->setParameter(':name', '%'.(string)$multi.'%')
;
}
if (!is_null($price)) {
$andX->add(
$qb->expr()->eq('p.price', ':price')
);
$qb->setParameter(':price', (int)$price);
}
if ((string)$andX !== "") {
$qb->andWhere($andX);
}
$qb->addOrderBy('p.id', 'DESC');
return $qb->getQuery()->getResult();
}
はい、ここでようやくandX
とorX
の登場です。
1つずつ見ていきましょう。まずはここです。
$andX = $qb->expr()->andX();
AndXクラスのインスタンスを作成しています。
次はここです。ここが今回の肝になる部分です。
if (!is_null($multi)) {
$andX->add(
$qb->expr()->orX(
$qb->expr()->eq('p.id', ':id'),
$qb->expr()->like('p.name', ':name')
)
);
$qb->setParameter(':id', (int)$multi)
->setParameter(':name', '%'.(string)$multi.'%')
;
}
if (!is_null($price)) {
$andX->add(
$qb->expr()->eq('p.price', ':price')
);
$qb->setParameter(':price', (int)$price);
}
この辺りで、実際に条件を指定しています。
DQLに直すとこんな感じ。
(前略) (id = :id or name like :name) and price = :price (後略)
:id
、:name
、:price
の部分に、フォームに入力した商品IDや名前、料金の値が入るわけですね。
上記のコードでは、findMany
メソッドで引数に取っている $multi
と$price
がNULLでない場合 に条件が追加されます。
(太字にした部分を覚えておいてください!)
最後はこの部分。
if ((string)$andX !== "") {
$qb->andWhere($andX);
}
andWhere
メソッドが出てきました。これでwhere
から始まる条件式の完成ですね。
ただ、if文の中にあります。このif文の条件は、
(string)$andX !== ""
となっています。$andX
が文字列でない場合に限り、andWhere
メソッドを実行するということですね。
さて、ひとつ前の解説で、「$multi
と$price
がNULLでない場合 に条件が追加される」と書きました。
つまり、$multi
と$price
がどちらもNULLなら、andWhere
メソッドを実行すると以下のようなクエリになってしまいます。
select * from product where;
これではSyntax error
になってしまいますね。条件が指定されていない場合は、where
を消すようにしたいです。
この、「条件が指定されていない」ことを判定しているのが、上記のif文です。
add
メソッドで追加した条件式は、AndXのインスタンス($andX
)で文字列となって保持されます。
つまり、条件が指定されていない場合、文字列は空ということになりますね。
逆に言えば、空でないということは何か条件が追加されているということですから、where
があってもエラーになりません。
では、このRepositoryのメソッドをControllerで使って、フォームに入力した値で検索できるようにしていきましょう。
商品一覧の画面を作ったときに何も書かなかったif文の中にコードを追加していきます。
/**
* @Route("/product", name="product_index", methods={"get", "post"})
*/
public function index(Request $request): Response
{
$form = $this->createFormBuilder()
->add('multi', TextType::class, [
'label' => "Product Id or Name",
'required' => false,
])
->add('price', MoneyType::class, [
'currency' => 'JPY',
'label' => "Product Price",
'required' => false,
])
->add('submit', SubmitType::class)
->getForm()
;
// 以下のコードを追加
$searchMulti = null;
$searchPrice = null;
if ("POST" === $request->getMethod()) {
$params = $request->get('form');
$searchMulti = empty($params['multi']) ? null : $params['multi'];
$searchPrice = empty($params['price']) ? null : $params['price'];
}
$products = $productRepository->findMany($searchMulti, $searchPrice);
return $this->render('and_x/index.html.twig', [
'form' => $form->createView(),
// 以下のコードを追加
'products' => $products,
]);
}
追加したコードの中で、検索機能に関わる部分を見ていきます。
$searchMulti = null;
$searchPrice = null;
if ("POST" === $request->getMethod()) {
$params = $request->get('form');
$searchMulti = empty($params['multi']) ? null : $params['multi'];
$searchPrice = empty($params['price']) ? null : $params['price'];
}
$products = $productRepository->findMany($searchMulti, $searchPrice);
ここですね。
と言っても、そんなに難しいことはしてません。
フォームに入力した値がPOSTで送信されてきたら、その値を先ほど作ったProductRepositoryのfindMany
メソッドに渡して検索処理を実行してもらっているだけです。
フォームが空だったらNULL
を、そうでなければフォームごとの値を取り出して、メソッドに渡しています。
最後に、検索した商品情報をtwigで受け取って表示するようにしてきましょう。
Controllerと同じく商品一覧画面の時に書きかけていた部分にコードを追記していきます。
ついでに、商品作成画面への動線も作っておきます。
{% block body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Product Index</h1>
{# 検索フォーム #}
<form action="{{url('product_index')}}" method="post">
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
</form>
<a href="{{url('product_index')}}"><p>Reset</p></a>
{# 商品作成画面への動線 #}
<a href="{{url('product_new')}}"><p>Create new product</p></a>
{# 商品が見つからなければ"Not found"のメッセージを、 #}
{# 見つかったら商品情報を表にして表示する #}
{% if products == null %}
<p>Not found..</p>
{% else %}
<p>Found products.</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>NAME</th>
<th>PRICE</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr>
<td>{{product.getId}}</td>
<td>{{product.getName}}</td>
<td>{{product.getPrice}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
{% endblock %}
こうですね。
商品は複数取得するので、for文で繰り返して1つずつ表示します。
getId
やgetName
、getPrice
といったメソッドは、Entityを作成する際に自動で作成されているものをそのまま使います。
CSSは、必要に応じてお好みで調整してください。
ここまで出来たら、こんな感じの画面が確認できるようになっているはずです。
-
商品を作成済みの場合
-
商品が未作成の場合
検索機能も実装しましたので、試してみましょう。
以下の例では商品IDで検索を行なっていますが、もちろん商品名や料金での検索も可能です。
これで、商品の新規作成と検索の実装が終わりました。管理画面っぽいものの完成です!
最後に
長々とお付き合いいただきありがとうございました。
本当はRepositoryのメソッドだけ見せる予定だったんですが、せっかくだし検証も兼ねてちゃんと形になったものを作って手順もしっかりまとめちゃおう〜と思った結果こうなりました。
案件で実際にSQL直書きでどうにか複雑な条件式を書いていたことがあったんですが、andX
とorX
使ったらコードがめちゃくちゃスッキリしたというか、「SQL直書きってやっぱりダサかったんだな!(個人の感想です)」といった感じで非常に感銘を受けました(?)。
Exprクラスには他にも便利なメソッドがたくさんあるので、実際に使って覚えていきたいですね。
株式会社 カラビナテクノロジーは「命綱や支点を素早く確実に繋ぐカラビナ。そんなカラビナのような役割をテクノロジーで実現したい」という想いのもと、福岡で設立。 主にシステム開発・アプリ開発・ Webサイト制作を行っています。採用情報→karabiner.tech/recruit/requirements/
Discussion