叫ぶうさぎの悪ふざけ

うさぎが目印のWebエンジニアが、得たことや思ったことを言の葉に乗せて叫ぶ場所です。

学習記録:12月9日(日):【輪読会まとめ 後編パート1】Laravel Webアプリケーション開発 Chapter5 データベース

これは 俺のインプットアウトプット記録 Advent Calendar 2018 の9日目のエントリーです。

昨日に引き続きです。分量増えちゃった。しかも後編はパート1と2に分かれるほどの分量になってしまいました(汗)

というわけでクエリビルダから。僕にとってはEloquentよりこちらの方が馴染みがありますし、他のフレームワークを使っての開発もたくさんありますので、Laravelにロックインされないためにも、クエリビルダを使いたいなあと思ったりします。

が、昨日も書いた通りですので両方使ってみようと思ってます。

データベースを取り扱う場合に、可能な限り抽象化されているというのはやはり利点の方が勝るとも思うので、どうにかしてEloquent、クエリビルダの両方を使ってみて、結果を検証したいなって思いました。

ちなみに、クエリビルダの章については、全てのSQLとコードを実際に実行したので、その結果も掲載している部分があります。冗長な部分がありますがご容赦くださいませ。あと、この章のSQL、コードは全部動くはずです。(実行し直したので大丈夫…のはず)

前置き

Laravelには tinker っていう便利なやつがあります。DBを操作するコードの実行には全てtinkerを使っています。一応、tinkerを使うところはコマンドから記載しているので大丈夫かとは思いますが…。

ちなみに、tinkerを実行する時は、Dockerコンテナの中に入り、artisanがある場所で実行しています。

$ docker-compose up -d nginx mysql workspace
(中略)
$ docker-compose exec --user=laradock workspace bash
$ pwd
/var/www
laradock@0f043864bf49:/var/www$ ls -la artisan 
-rw-r--r-- 1 laradock laradock 1686 Oct 16 16:35 artisan
laradock@0f043864bf49:/var/www$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> 

5-4 クエリビルダ

  • クエリビルダのメソッドを繋げてSQLを組み立てる
  • メソッドチェーン
  • Eloquentも内部にクエリビルダのインスタンスを持つ

リスト5.4.0.1:データ検索のSQL

  • 価格が1000円以上、かつ出版日が 2011-01-01 以降
  • これをクエリビルダで書き換えていく
SELECT
    bookdetails.isbn, 
    books.name, 
    authors.name,
    bookdetails.price
FROM 
    books
LEFT JOIN 
    bookdetails 
    ON books.bookdetail_id = bookdetails.id
LEFT JOIN 
    authors 
    ON books.author_id = authors.id
WHERE 
    bookdetails.price >= 1000 
AND 
    bookdetails.published_date >= '2011-01-01'
ORDER BY 
    bookdetails.published_date DESC;

リスト5.4.0.2:データ検索のSQL文をクエリビルダで組み立てる

  • ①ベースのbooksテーブルのクエリビルダインスタンスを取得
  • ②取得カラムを指定
  • ③テーブル結合
  • ④〜⑤条件指定
  • ⑥ソート
    • ここまでの各メソッドの戻り値は Illuminate\Database\Query\Builder
    • つまりクエリビルダを戻り値に受け取り続けることでメソッドチェーンが可能
  • SQL実行して結果のオブジェクトを取得
    • ここが呼ばれるまでDB接続もSQL実行もされない
    • 実行結果は stdClass オブジェクトのコレクションで取り扱いも容易
      • 各カラムを操作するのも簡単
<?php
$results = DB::table('books')                                                  //①
->select(['bookdetails.isbn','books.name','authors.name','bookdetails.price']) //②
->leftjoin('bookdetails', 'books.bookdetail_id', '=', 'bookdetails.id')        //③
->leftjoin('authors', 'books.author_id', '=', 'authors.id')                    //③
->where('bookdetails.price', '>=', 1000)                                       //④
->where('bookdetails.published_date', '>=', '2011-01-01')                      //⑤
->orderby('bookdetails.published_date', 'desc')                                //⑥
->get();                                                                       //⑦
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->select(['bookdetails.isbn', 'books.name', 'authors.name', 'bookdetails.price'])->leftjoin('bookdetails', 'books.bookdetail_id', '=', 'bookdetails.id')->leftjoin('authors', 'books.author_id', '=', 'authors.id')->where('bookdetails.price', '>=', 1000)->where('bookdetails.published_date', '>=', '2011-01-01')->orderby('bookdetails.published_date', 'desc')->get();
=> Illuminate\Support\Collection {#2917
     all: [
       {#2918
         +"isbn": "9784204176365",
         +"name": "著者名9",
         +"price": 3755,
       },
       {#2920
         +"isbn": "9793433402596",
         +"name": "著者名2",
         +"price": 3962,
       },
     ],
   }
>>> 

5-4-1 クエリビルダの書式

  • ①ベースとなるクエリビルダオブジェクト
  • ②〜⑥メソッドチェーンによる処理対象や内容の特定
  • ⑦クエリ実行
  • この書式は検索、更新、削除でも共通

tinkerによるクエリ実行

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->select(['bookdetails.isbn', 'books.name', 'authors.name', 'bookdetails.price', 'bookdetails.published_date'])->leftjoin('bookdetails', 'books.bookdetail_id', '=', 'bookdetails.id')->leftjoin('authors', 'book                                                                                                                                                                          desc')->get();
=> Illuminate\Support\Collection {#2884
     all: [
       {#2922
         +"isbn": "9784204176365",
         +"name": "著者名9",
         +"price": 3755,
         +"published_date": "2018-08-07",
       },
       {#2926
         +"isbn": "9793433402596",
         +"name": "著者名2",
         +"price": 3962,
         +"published_date": "2015-05-16",
       },
       {#2924
         +"isbn": "9784238339682",
         +"name": "著者名2",
         +"price": 9908,
         +"published_date": "2009-02-24",
       },
       {#2928
         +"isbn": "9790930291296",
         +"name": "著者名7",
         +"price": 7689,
         +"published_date": "2006-05-11",
       },
       {#2879
         +"isbn": "9788521682677",
         +"name": "著者名7",
         +"price": 9093,
         +"published_date": "2000-11-14",
       },
       {#2874
         +"isbn": "9787518986811",
         +"name": "著者名3",
         +"price": 1634,
         +"published_date": "2000-05-10",
       },
     ],
   }

5-4-2- クエリビルダの取得

リスト5.4.2.1:DBファサードを利用したクエリビルダの取得

<?php

// 書籍テーブルのクエリビルダ取得
$query = DB::table('books');

リスト5.4.2.2:Connectionクラスからクエリビルダを取得

<?php

// ①サービスコンテナからDatabaseManagerクラスのインスタンス取得
$db = \Illuminate\Foundation\Application::getInstance()->make('db');

// ②上記インスタンスからConnectionクラスのインスタンスを取得
$connection = $db->connection();

// ③Connectionクラスのインスタンスからクエリビルダを取得
$query = $connection->table('books');

リスト5.4.2.3:データ操作専用クラスを作ってクエリビルダを利用

  • コンストラクタインジェクションを利用
  • クエリビルダ提供元のクラスを外から与える
  • 拡張性やテスト容易性を保つことが可能
<?php
declare(strict_types=1);

namespace App\DataAccess;

use Illuminate\Database\DatabaseManager;

class BookDataAccessObject
{
    /** @var DatabaseManager */
    protected $db;

    /** @var string */
    protected $table = 'books';

    public function __construct(DatabaseManager $db)
    {
        $this->db = $db;
    }

    public function find($id)
    {
        $query = $this->db->connection()
            ->table($this->table);
        (以下略)
    }
}

5-4-3 処理対象や内容の特定

  • 各種メソッドの紹介と実装例

Select系メソッド

表5.4.3.1:Select系メソッド

メソッド 機能
select(カラム名の配列) 取得対象のカラム名を指定する
selectRaw(SQL文) select文の中身をSQLで直接指定する

リスト5.4.3.2:Select系メソッドの利用

<?php

$result = DB::table('books')->select('id', 'name as title')->get();

$result = DB::table('books')->selectRaw('id, name as title')->get();
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->select('id', 'name as title')->get();
=> Illuminate\Support\Collection {#2881
     all: [
       {#2872
         +"id": 11,
         +"title": "Quae sunt libero adipisci.",
       },
       {#2859
         +"id": 12,
         +"title": "Est nihil ut quo.",
       },
       {#2868
         +"id": 13,
         +"title": "Quas maiores sit aut.",
       },
       {#2867
         +"id": 14,
         +"title": "Quibusdam ut eius omnis.",
       },
       {#2874
         +"id": 15,
         +"title": "Animi et sed culpa.",
       },
       {#2869
         +"id": 16,
         +"title": "Dignissimos sunt est autem ea.",
       },
       {#2882
         +"id": 17,
         +"title": "Minus cum ut est.",
       },
       {#2860
         +"id": 18,
         +"title": "At voluptates numquam magni.",
       },
       {#2853
         +"id": 19,
         +"title": "Ratione dolore exercitationem.",
       },
       {#2852
         +"id": 20,
         +"title": "Repellat molestiae provident nisi.",
       },
     ],
   }
>>> DB::table('books')->selectRaw('id, name as title')->get();
=> Illuminate\Support\Collection {#2879
     all: [
       {#2840
         +"id": 11,
         +"title": "Quae sunt libero adipisci.",
       },
       {#2856
         +"id": 12,
         +"title": "Est nihil ut quo.",
       },
       {#2858
         +"id": 13,
         +"title": "Quas maiores sit aut.",
       },
       {#2871
         +"id": 14,
         +"title": "Quibusdam ut eius omnis.",
       },
       {#2865
         +"id": 15,
         +"title": "Animi et sed culpa.",
       },
       {#2850
         +"id": 16,
         +"title": "Dignissimos sunt est autem ea.",
       },
       {#2866
         +"id": 17,
         +"title": "Minus cum ut est.",
       },
       {#2870
         +"id": 18,
         +"title": "At voluptates numquam magni.",
       },
       {#2861
         +"id": 19,
         +"title": "Ratione dolore exercitationem.",
       },
       {#2855
         +"id": 20,
         +"title": "Repellat molestiae provident nisi.",
       },
     ],
   }
>>> 

Where系メソッド

  • 連続して呼ぶとAND条件
  • ORにしたければ orWhere orWhereBetween など先頭に or をつける

表5.4.3.3:Where系メソッド

メソッド 機能
where(`カラム名', ''比較演算子, '条件値') whereを使用した一般的な条件指定。比較演算子を省略すると等価判定(=)となる
whereBetween('カラム名', '範囲') betweenを使用した範囲指定
whereNotBetween('カラム名', '範囲') not betweenを使用した範囲指定
whereIn('カラム名', '条件値') inを使用
whereNotIn('カラム名', '条件値') not inを使用
whereNull(カラム名) is nullを使用
whereNotNull(カラム名) is not nullを使用

リスト5.4.3.4:Where系メソッドの利用

<?php

$results = DB::table('books')
    ->where('id', '>=', '30')
    ->orWhere(`created_at`, '>=', '2018-01-01')
    ->get();
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->where('id', '>=', '30')->orWhere('created_at', '>=', '2018-01-01')->get();
=> Illuminate\Support\Collection {#2857
     all: [
       {#2858
         +"id": 11,
         +"name": "Quae sunt libero adipisci.",
         +"bookdetail_id": 17,
         +"author_id": 23,
         +"publisher_id": 12,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2856
         +"id": 12,
         +"name": "Est nihil ut quo.",
         +"bookdetail_id": 30,
         +"author_id": 11,
         +"publisher_id": 12,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2849
         +"id": 13,
         +"name": "Quas maiores sit aut.",
         +"bookdetail_id": 25,
         +"author_id": 32,
         +"publisher_id": 9,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2879
         +"id": 14,
         +"name": "Quibusdam ut eius omnis.",
         +"bookdetail_id": 10,
         +"author_id": 8,
         +"publisher_id": 15,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2840
         +"id": 15,
         +"name": "Animi et sed culpa.",
         +"bookdetail_id": 49,
         +"author_id": 17,
         +"publisher_id": 10,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2851
         +"id": 16,
         +"name": "Dignissimos sunt est autem ea.",
         +"bookdetail_id": 38,
         +"author_id": 39,
         +"publisher_id": 5,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2861
         +"id": 17,
         +"name": "Minus cum ut est.",
         +"bookdetail_id": 32,
         +"author_id": 1,
         +"publisher_id": 8,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2880
         +"id": 18,
         +"name": "At voluptates numquam magni.",
         +"bookdetail_id": 28,
         +"author_id": 37,
         +"publisher_id": 27,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2855
         +"id": 19,
         +"name": "Ratione dolore exercitationem.",
         +"bookdetail_id": 43,
         +"author_id": 22,
         +"publisher_id": 18,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2864
         +"id": 20,
         +"name": "Repellat molestiae provident nisi.",
         +"bookdetail_id": 50,
         +"author_id": 24,
         +"publisher_id": 10,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
     ],
   }
>>> 

Limit, Offsetメソッド

表5.4.3.5:LimitとOffsetメソッド

メソッド 機能
limit(数値) または take(数値) limit句に置き換わる
offset(数値) または skip(数値) offset句に置き換わる

リスト5.4.3.6:Limit, Offsetメソッドの利用

<?php

$results = DB::table('books')
    ->limit(10)
    ->offset(6)
    ->get();
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->limit(10)->offset(6)->get();
=> Illuminate\Support\Collection {#2865
     all: [
       {#2872
         +"id": 17,
         +"name": "Minus cum ut est.",
         +"bookdetail_id": 32,
         +"author_id": 1,
         +"publisher_id": 8,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2871
         +"id": 18,
         +"name": "At voluptates numquam magni.",
         +"bookdetail_id": 28,
         +"author_id": 37,
         +"publisher_id": 27,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2866
         +"id": 19,
         +"name": "Ratione dolore exercitationem.",
         +"bookdetail_id": 43,
         +"author_id": 22,
         +"publisher_id": 18,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2870
         +"id": 20,
         +"name": "Repellat molestiae provident nisi.",
         +"bookdetail_id": 50,
         +"author_id": 24,
         +"publisher_id": 10,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
     ],
   }
>>> 

集計系メソッド

SQLを生で書くことが多い僕にはお馴染みのメソッドたちです。

表5.4.3.7:集計系メソッド

メソッド 機能
orderBy(カラム名, 方向) order by句に置き換わる
groupBy(カラム名) group by句に置き換わる
having(`カラム名’, '比較演算子', '条件値') havingを利用した絞り込み
havingRaw(SQL文) having句の中身をSQLで直接指定

リスト5.4.3.8:集計系の中から、orderByメソッドによる複数カラムのソート

<?php

$results = DB::table('books')
    ->orderBy('id')
    ->orderBy('updated_at', 'desc')
    ->get();
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->orderBy('id')->orderBy('updated_at', 'desc')->get();
=> Illuminate\Support\Collection {#2879
     all: [
       {#2861
         +"id": 11,
         +"name": "Quae sunt libero adipisci.",
         +"bookdetail_id": 17,
         +"author_id": 23,
         +"publisher_id": 12,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2880
         +"id": 12,
         +"name": "Est nihil ut quo.",
         +"bookdetail_id": 30,
         +"author_id": 11,
         +"publisher_id": 12,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2849
         +"id": 13,
         +"name": "Quas maiores sit aut.",
         +"bookdetail_id": 25,
         +"author_id": 32,
         +"publisher_id": 9,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2859
         +"id": 14,
         +"name": "Quibusdam ut eius omnis.",
         +"bookdetail_id": 10,
         +"author_id": 8,
         +"publisher_id": 15,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2858
         +"id": 15,
         +"name": "Animi et sed culpa.",
         +"bookdetail_id": 49,
         +"author_id": 17,
         +"publisher_id": 10,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2857
         +"id": 16,
         +"name": "Dignissimos sunt est autem ea.",
         +"bookdetail_id": 38,
         +"author_id": 39,
         +"publisher_id": 5,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2856
         +"id": 17,
         +"name": "Minus cum ut est.",
         +"bookdetail_id": 32,
         +"author_id": 1,
         +"publisher_id": 8,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2851
         +"id": 18,
         +"name": "At voluptates numquam magni.",
         +"bookdetail_id": 28,
         +"author_id": 37,
         +"publisher_id": 27,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2840
         +"id": 19,
         +"name": "Ratione dolore exercitationem.",
         +"bookdetail_id": 43,
         +"author_id": 22,
         +"publisher_id": 18,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2847
         +"id": 20,
         +"name": "Repellat molestiae provident nisi.",
         +"bookdetail_id": 50,
         +"author_id": 24,
         +"publisher_id": 10,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
     ],
   }
>>> 

JOINメソッド

フレームワークにより記法が異なりますが、パッと見でわかる感じですね。

表5.4.3.9:JOINメソッド

メソッド 機能
join('対象テーブル', '結合対象カラム', '演算子', '結合対象カラム') テーブル間の内部結合、inner joinに置き換わる
leftJoin('対象テーブル', '結合対象カラム', '演算子', '結合対象カラム') テーブル間の外部結合、left joinに置き換わる
rightJoin('対象テーブル', '結合対象カラム', '演算子', '結合対象カラム') テーブル間の外部結合、right joinに置き換わる

リスト5.4.3.10:連続したJOINメソッドによる結合

tinkerでお手軽に実行します。ワンライナーにしてるので実際にコードに書く場合と分けてみます。

<?php

$results = DB::table('books')
                ->leftJoin('authors', 'books.author_id', '=', 'authors.id')
                ->leftJoin('publishers', 'books.publisher_id', '=', 'publishers.id')
                ->get();
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->leftJoin('authors', 'books.author_id', '=', 'authors.id')->leftJoin('publishers', 'books.publisher_id', '=', 'publishers.id')->get();
=> Illuminate\Support\Collection {#2872
     all: [
       {#2869
         +"id": 12,
         +"name": "株式会社 青田出版",
         +"bookdetail_id": 17,
         +"author_id": 23,
         +"publisher_id": 12,
         +"created_at": "2018-11-14 16:36:06",
         +"updated_at": "2018-11-14 16:36:06",
         +"kana": "チョシャメイ3",
         +"deleted_at": null,
         +"address": "4586817  群馬県青山市南区中津川町松本1-5-2",
       },
       {#2874
         +"id": 12,
         +"name": "株式会社 青田出版",
         +"bookdetail_id": 30,
         +"author_id": 11,
         +"publisher_id": 12,
         +"created_at": "2018-11-14 16:36:06",
         +"updated_at": "2018-11-14 16:36:06",
         +"kana": "チョシャメイ1",
         +"deleted_at": null,
         +"address": "4586817  群馬県青山市南区中津川町松本1-5-2",
       },
       {#2867
         +"id": 9,
         +"name": "有限会社 佐々木出版",
         +"bookdetail_id": 25,
         +"author_id": 32,
         +"publisher_id": 9,
         +"created_at": "2018-11-14 15:56:28",
         +"updated_at": "2018-11-14 15:56:28",
         +"kana": "チョシャメイ2",
         +"deleted_at": null,
         +"address": "8481973  群馬県山田市北区宇野町喜嶋6-2-10",
       },
       {#2868
         +"id": 15,
         +"name": "株式会社 山田出版",
         +"bookdetail_id": 10,
         +"author_id": 8,
         +"publisher_id": 15,
         +"created_at": "2018-11-14 16:36:06",
         +"updated_at": "2018-11-14 16:36:06",
         +"kana": "チョシャメイ8",
         +"deleted_at": null,
         +"address": "7517039  山梨県工藤市北区小林町三宅2-7-10",
       },
       {#2870
         +"id": 10,
         +"name": "株式会社 田辺出版",
         +"bookdetail_id": 49,
         +"author_id": 17,
         +"publisher_id": 10,
         +"created_at": "2018-11-14 15:56:28",
         +"updated_at": "2018-11-14 15:56:28",
         +"kana": "チョシャメイ7",
         +"deleted_at": null,
         +"address": "3881619  高知県津田市中央区喜嶋町宮沢9-1-7 ハイツ藤本110号",
       },
       {#2866
         +"id": 5,
         +"name": "株式会社 青山出版",
         +"bookdetail_id": 38,
         +"author_id": 39,
         +"publisher_id": 5,
         +"created_at": "2018-11-14 15:56:28",
         +"updated_at": "2018-11-14 15:56:28",
         +"kana": "チョシャメイ9",
         +"deleted_at": null,
         +"address": "2219948  千葉県西之園市東区井上町佐々木4-5-8 ハイツ井高110号",
       },
       {#2871
         +"id": 8,
         +"name": "株式会社 吉本出版",
         +"bookdetail_id": 32,
         +"author_id": 1,
         +"publisher_id": 8,
         +"created_at": "2018-11-14 15:56:28",
         +"updated_at": "2018-11-14 15:56:28",
         +"kana": "チョシャメイ1",
         +"deleted_at": null,
         +"address": "9972025  福井県廣川市東区吉本町笹田5-4-2 ハイツ青山104号",
       },
       {#2865
         +"id": 27,
         +"name": "有限会社 井高出版",
         +"bookdetail_id": 28,
         +"author_id": 37,
         +"publisher_id": 27,
         +"created_at": "2018-11-14 16:36:18",
         +"updated_at": "2018-11-14 16:36:18",
         +"kana": "チョシャメイ7",
         +"deleted_at": null,
         +"address": "7757641  秋田県宇野市東区西之園町小泉9-4-4 ハイツ野村107号",
       },
       {#2864
         +"id": 18,
         +"name": "有限会社 西之園出版",
         +"bookdetail_id": 43,
         +"author_id": 22,
         +"publisher_id": 18,
         +"created_at": "2018-11-14 16:36:06",
         +"updated_at": "2018-11-14 16:36:06",
         +"kana": "チョシャメイ2",
         +"deleted_at": null,
         +"address": "8728464  岐阜県鈴木市西区近藤町江古田1-3-8",
       },
       {#2863
         +"id": 10,
         +"name": "株式会社 田辺出版",
         +"bookdetail_id": 50,
         +"author_id": 24,
         +"publisher_id": 10,
         +"created_at": "2018-11-14 15:56:28",
         +"updated_at": "2018-11-14 15:56:28",
         +"kana": "チョシャメイ4",
         +"deleted_at": null,
         +"address": "3881619  高知県津田市中央区喜嶋町宮沢9-1-7 ハイツ藤本110号",
       },
     ],
   }
>>> 

5-4-4 クエリの実行

  • クエリビルダの最後に指定するSQL実行について

5-4-4-1 データの取得

こちらはお馴染みな感じがしますね。

表5.4.4.1:クエリを実行し結果を取得するメソッド

メソッド 機能 結果
get() 全てのデータを取得 stdClassオブジェクトのコレクション
first() 最初の1行 オブジェクト単体

リスト5.4.4.2:tinkerによるgetメソッド実行例

Seederを使ってbooksテーブルにダミーデータを入れてから実行しています。tinker便利ですね。

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->select('id', 'name')->get();
=> Illuminate\Support\Collection {#2847
     all: [
       {#2849
         +"id": 11,
         +"name": "Quae sunt libero adipisci.",
       },
       {#2846
         +"id": 12,
         +"name": "Est nihil ut quo.",
       },
       {#2845
         +"id": 13,
         +"name": "Quas maiores sit aut.",
       },
       {#2844
         +"id": 14,
         +"name": "Quibusdam ut eius omnis.",
       },
       {#2843
         +"id": 15,
         +"name": "Animi et sed culpa.",
       },
       {#2842
         +"id": 16,
         +"name": "Dignissimos sunt est autem ea.",
       },
       {#2855
         +"id": 17,
         +"name": "Minus cum ut est.",
       },
       {#2857
         +"id": 18,
         +"name": "At voluptates numquam magni.",
       },
       {#2858
         +"id": 19,
         +"name": "Ratione dolore exercitationem.",
       },
       {#2859
         +"id": 20,
         +"name": "Repellat molestiae provident nisi.",
       },
     ],
   }
>>> DB::table('books')->select('id', 'name')->first();
=> {#2862
     +"id": 11,
     +"name": "Quae sunt libero adipisci.",
   }
>>> DB::table('books')->select('id', 'name')->find(11);
=> {#2858
     +"id": 11,
     +"name": "Quae sunt libero adipisci.",
   }

実際のクエリ

-- getメソッド
select `id`, `name` from `books`

-- firstメソッド
select `id`, `name` from `books` limit 1

-- findメソッド(なぜかlimitがつく)
select `id`, `name` from `books` where `id` = 11 limit 1

表5.4.4.3:レコード数取得や計算を行うメソッド

集約関数と呼ばれるやつですね。

メソッド 機能
count() 件数取得
max(カラム名) 最大数取得
min(カラム名) 最小値
avg(カラム名) 平均値

リスト5.4.4.4:tinkerによる各メソッド実行例

こちらもtinkerで実行します。

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('bookdetails')->count();
=> 100
>>> DB::table('bookdetails')->max('price');
=> 9918
>>> DB::table('bookdetails')->min('price');
=> 66
>>> DB::table('bookdetails')->avg('price');
=> "5193.3600"

5-4-4-2 データの登録、更新、削除

こちらもお馴染みです。

表5.4.4.4:データ更新を行うメソッド

メソッド 機能
insert(['カラム'=>'値', ...]) データ登録
update(['カラム'=>'値', ...]) データ更新
delete() データ削除
truncate() 全行削除

リスト5.4.4.5:データ更新

こちらもtinkerで実行してみます。

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('bookdetails')->where('id', 1)->update(['price' => 10000]);
=> 1
>>> DB::table('bookdetails')->max('price');
=> 10000

5-4-5 トランザクションとテーブルロック

トランザクション、テーブルロックといった操作も可能です。残念ながら僕は未だ ロックを意図的に実施する必要があるシステム を経験したことがないのですが、想定されるおおよそのSQLがメソッドとして用意されているのはパワフルだな、と思いました。

表5.4.5.1:トランザクション系メソッド

メソッド 機能
DB::beginTransaction() 手動でトランザクション開始
DB::rollback() 手動ロールバック
DB::commit() 手動コミット
DB::transaction(クロージャ) クロージャの中でトランザクションを実施

手動トランザクションの実例

せっかくなので、トランザクションの動きをなぞって検証してみます。

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::beginTransaction();
=> null

トランザクション直後のデータ

mysql> select * from bookdetails where id=100;
+-----+---------------+----------------+-------+---------------------+---------------------+
| id  | isbn          | published_date | price | created_at          | updated_at          |
+-----+---------------+----------------+-------+---------------------+---------------------+
| 100 | 9780493063348 | 1998-02-06     |  3265 | 2018-11-18 07:00:03 | 2018-11-18 07:00:03 |
+-----+---------------+----------------+-------+---------------------+---------------------+
1 row in set (0.00 sec)

UPDATE実施(未コミット)

>>> DB::table('bookdetails')->where('id', 100)->update(['price' => 100000]);
=> 1

UPDATEしたが、commitしていない状態、値段は変わっていない

mysql> select * from bookdetails where id=100;
+-----+---------------+----------------+-------+---------------------+---------------------+
| id  | isbn          | published_date | price | created_at          | updated_at          |
+-----+---------------+----------------+-------+---------------------+---------------------+
| 100 | 9780493063348 | 1998-02-06     |  3265 | 2018-11-18 07:00:03 | 2018-11-18 07:00:03 |
+-----+---------------+----------------+-------+---------------------+---------------------+
1 row in set (0.00 sec)

commit実施

>>> DB::commit();
=> null

UPDATEし、commitした結果、値段が変わっている

mysql> select * from bookdetails where id=100;
+-----+---------------+----------------+--------+---------------------+---------------------+
| id  | isbn          | published_date | price  | created_at          | updated_at          |
+-----+---------------+----------------+--------+---------------------+---------------------+
| 100 | 9780493063348 | 1998-02-06     | 100000 | 2018-11-18 07:00:03 | 2018-11-18 07:00:03 |
+-----+---------------+----------------+--------+---------------------+---------------------+
1 row in set (0.00 sec)

表5.4.5.2:悲観的ロックのメソッド

こちらはなんとなく利用シーンが思い浮かぶ気がするんですが、じゃあ具体的にどんな時?と言われるとパッと出てきません。まだまだだな、と思うと以前の僕ならこの章のように悲観的になったりもしたのですが、今の僕は「楽しみだな」と思たりはします。

ここではメソッドの紹介に止まり、実際に動かすところまでは書いていませんでした。 この本は2週目を考えているので、その際にシチュエーションと実際の実装をやりたいですね。

メソッド 機能
sharedLock() selectされた行に共有ロックをかけ、トランザクションコミットまで更新を禁止する
lockForUpdate() selectされた行に排他ロックをかけ、トランザクションコミットまで読み書き両方を禁止する

5-4-6 ベーシックなデータ操作

  • EloquentもクエリビルダもSQLに変換されているよね
  • コードから生成されるSQLが常に一定とは限らない
    • 個人的にこれは困るなあ…
  • クエリが長いとメソッドチェーンも長くなるので手軽さや可読性が失われる
  • SQL直接書きたい!
  • ありますよ。

表5.4.6.1:ベーシックなSQL実行メソッド

あらゆるSQLが書けるようなメソッドが用意されてます。プリペアドステートメントもPDOで親しんだのと同じなので、速度を求められる場合や複雑な分析系のSQLを書く際には重宝しそうです。(SQLが複雑な時点で設計が…という話は置いといて)

メソッド名 説明
DB::select('selectクエリ', [クエリに結合する引数]) select文によるデータ抽出
DB::insert('insertクエリ', [クエリに結合する引数]) insert文によるデータ登録
DB::update('updateクエリ', [クエリに結合する引数]) update文によるデータ更新。更新された行数が返る
DB::delete('deleteクエリ', [クエリに結合する引数]) delete文によるデータ削除。削除された行数が返る
DB::statement('SQL', [クエリに結合する引数]) 上記以外のSQLを実行する場合

コード5.4.6.1:DB:selectを使用したデータ抽出

以下のSQLを実行します。

mysql> SELECT 
    ->     bookdetails.isbn, books.name 
    -> FROM 
    ->     books 
    -> LEFT JOIN 
    ->     bookdetails 
    ->         ON books.bookdetail_id = bookdetails.id 
    -> WHERE 
    ->     bookdetails.price >= '1000' 
    -> AND bookdetails.published_date >= '2011-01-01' 
    -> ORDER BY 
    ->     bookdetails.published_date DESC;
+---------------+--------------------------------+
| isbn          | name                           |
+---------------+--------------------------------+
| 9784204176365 | Dignissimos sunt est autem ea. |
| 9793433402596 | Quas maiores sit aut.          |
+---------------+--------------------------------+
2 rows in set (0.00 sec)

コードはこちら。 ちなみに //利用方法 の部分、配列の中は stdClassオブジェクト なのでアロー演算子じゃないとエラーになります。ので修正してます。

<?php
$sql =  ' SELECT bookdetails.isbn, books.name '
    .   ' FROM books'
    .   ' LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id'
    .   ' WHERE bookdetails.price >= ? AND bookdetails.published_date >= ?'
    .   ' ORDER BY bookdetails.published_date DESC';

$results = DB::select($sql, [1000, '2011-01-01']);

// 僕はこっち派
$sql =  ' SELECT bookdetails.isbn, books.name '
    .   ' FROM books'
    .   ' LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id'
    .   ' WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date'
    .   ' ORDER BY bookdetails.published_date DESC';

$results = DB::select($sql, ['price' => 1000, 'published_date' => '2011-01-01']);

// 利用方法、$bookはstdClassオブジェクトなのでアロー演算子だよ
foreach ($results as $book) {
    echo $book->name;
    echo $book->isbn;
}

tinkerで実行します。 tinkerは改行を入れるとエラーになりますが、パッと確認できる最小単位なので便利なので一貫して使ってます。

>>> $sql =  'SELECT bookdetails.isbn, books.name FROM books LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date ORDER BY bookdetails.published_date DESC';
=> "SELECT bookdetails.isbn, books.name FROM books LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date ORDER BY bookdetails.published_date DESC"
>>> $results = DB::select($sql, ['price' => 1000, 'published_date' => '2011-01-01']);
=> [
     {#2864
       +"isbn": "9784204176365",
       +"name": "Dignissimos sunt est autem ea.",
     },
     {#2863
       +"isbn": "9793433402596",
       +"name": "Quas maiores sit aut.",
     },
   ]
>>> foreach ($results as $book) { echo $book->name; echo $book->isbn; }
Dignissimos sunt est autem ea.9784204176365Quas maiores sit aut.9793433402596
>>> 

foreachでの stdClass オブジェクトへのアクセスも確認できます。

コード5.4.6.2:PDOオブジェクトを直接使用する

ほぼ同じなので写経して動かしたものを貼っておきます。

<?php
$sql =  ' SELECT bookdetails.isbn, books.name '
    .   ' FROM books'
    .   ' LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id'
    .   ' WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date'
    .   ' ORDER BY bookdetails.published_date DESC';

$pdo = DB::connect()->getPdo();
$statement = $pdo->prepare($sql);
$statement->execute(['price' => 1000, 'published_date' => '2011-01-01']);
$results = $statement->fetchAll(PDO::FETCH_ASSOC);

// 利用方法、PDOだと配列
foreach ($results as $book) {
    echo $book['name'];
    echo $book['isbn'];
}
>>> $sql =  'SELECT bookdetails.isbn, books.name FROM books LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date ORDER BY bookdetails.published_date DESC';
=> "SELECT bookdetails.isbn, books.name FROM books LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date ORDER BY bookdetails.published_date DESC"
>>> $pdo = DB::connection()->getPdo();
=> PDO {#2850
     inTransaction: false,
     attributes: {
       CASE: NATURAL,
       ERRMODE: EXCEPTION,
       AUTOCOMMIT: 1,
       PERSISTENT: false,
       DRIVER_NAME: "mysql",
       SERVER_INFO: "Uptime: 10737  Threads: 2  Questions: 77  Slow queries: 0  Opens: 125  Flush tables: 1  Open tables: 118  Queries per second avg: 0.007",
       ORACLE_NULLS: NATURAL,
       CLIENT_VERSION: "mysqlnd 5.0.12-dev - 20150407 - $Id: 38fea24f2847fa7519001be390c98ae0acafe387 $",
       SERVER_VERSION: "5.7.24",
       STATEMENT_CLASS: [
         "PDOStatement",
       ],
       EMULATE_PREPARES: 0,
       CONNECTION_STATUS: "mysql via TCP/IP",
       DEFAULT_FETCH_MODE: BOTH,
     },
   }
>>> $statement = $pdo->prepare($sql);
=> PDOStatement {#2844
     +queryString: "SELECT bookdetails.isbn, books.name FROM books LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date ORDER BY bookdetails.published_date DESC",
   }
>>> $statement->execute(['price' => 1000, 'published_date' => '2011-01-01']);
=> true
>>> $results = $statement->fetchAll(PDO::FETCH_ASSOC);
=> [
     [
       "isbn" => "9784204176365",
       "name" => "Dignissimos sunt est autem ea.",
     ],
     [
       "isbn" => "9793433402596",
       "name" => "Quas maiores sit aut.",
     ],
   ]
>>> foreach ($results as $book) { echo $book['isbn']; echo $book['name']; }
9784204176365Dignissimos sunt est autem ea.9793433402596Quas maiores sit aut.
>>>

対話型でそれぞれ処理が実行され、無事に動いていることがわかります。

やってみて

実際にtinkerを使ってコードを実行していくと、変数や配列、オブジェクトに何が設定され、何が返ってくるのかがはっきりとわかりました。

コードを実行していて、これほど動きを実感できることってそうそうないよなって思います。

ずーっと昔、VisualCで業務システムのコードを書いていた時に、デバッガを使ってメモリ確保の中身まで追いかけた日々を思い出します。

そしてEloquent(ORM)とクエリビルダ、どっちがいいか悪いかはないと思うに至りました。Laravelに特化してサービスを構築して、効率的にリリースを継続するなら、フレームワークの力を最大限に引き出すべきだろうと思いました。

が、そうではないレベルにサービスが成長したのなら、それはケースバイケース。その時に採り得る最良の方法で課題を解決していくために、技術を、道具を、選択していくべきだよな、と改めて思わせてくれました。

データベースの章だけでもこんな風に思わせてくれるんですから、通読すれば得られることはすごく多いと思います。Laravel本、と言いますけど、Laravelを通して設計やOOPについて基礎からしっかり学べる書籍になっていると思います。

もし興味があって、購入を迷われているかたがおられましたら、ぜひ購入してみてください。得られることはものすごくたくさんあると思います!

そして後半パート2へ続くのじゃ…(汗)

そしてなんと…ここまでで約3.4万文字。後編を2つに分けることに相成りました!

というわけで、リポジトリパターンは後編パート2に続く!!!