質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Laravel

LaravelとはTaylor Otwellによって開発された、オープンソースなPHPフレームワークです。Laravelはシンプルで表現的なシンタックスを持ち合わせており、ウェブアプリケーション開発の手助けをしてくれます。

Laravel 5

Laravel 5は、PHPフレームワークLaravelの最新バージョンで、2014年11月に発表予定です。ディレクトリ構造がが現行版より大幅に変更されるほか、メソッドインジェクションやFormRequestの利用が可能になります。

Q&A

解決済

3回答

3623閲覧

Laravel5.7 で不等号を使ってリレーションする方法

退会済みユーザー

退会済みユーザー

総合スコア0

Laravel

LaravelとはTaylor Otwellによって開発された、オープンソースなPHPフレームワークです。Laravelはシンプルで表現的なシンタックスを持ち合わせており、ウェブアプリケーション開発の手助けをしてくれます。

Laravel 5

Laravel 5は、PHPフレームワークLaravelの最新バージョンで、2014年11月に発表予定です。ディレクトリ構造がが現行版より大幅に変更されるほか、メソッドインジェクションやFormRequestの利用が可能になります。

3グッド

4クリップ

投稿2019/04/13 02:14

Laravel5.7, MySQL5.7 で見積書、および請求書を生成、管理するシステムを作成しています。
リレーション周辺の知見をいただきたく質問しました。

Laravel 職人のご意見をお聞かせください。

具体的には、見積モデルの「見積書発行日」時点での消費税モデルをどうリレーションするのか?自分が採用したアプローチの他にもっと良い方法はないのか?
という疑問です。

なぜ悩んでいるかを列挙すると、以下の通りです。

  • リレーションはできるだけすっきりと書きたい。(可読性のため)
  • トリッキーなことはしたくない。(Laravelの作法には従おう)
  • 発行日が変更されたとき、正しい消費税率を関連づけたい。できれば発行日が変更されても「よしなに」税率も変更される。
  • ビジネスロジックがブラックボックスになりがちだから、SQLのトリガーは使いたくない。

Model, migration

問題になる部分の関連 Modelは Estimate, Tax で、今のところ、以下のような構造です。

php

1<?php 2 3use Illuminate\Database\Migrations\Migration; 4use Illuminate\Database\Schema\Blueprint; 5use Illuminate\Support\Facades\Schema; 6 7class CreateEstimatesTable extends Migration 8{ 9 /** 10 * Run the migrations. 11 * 12 * @return void 13 */ 14 public function up() 15 { 16 Schema::create('estimates', function (Blueprint $table) { 17 $table->bigIncrements('id')->comment('ID'); 18 $table->unsignedBigInteger('company_id')->comment('会社ID'); 19 $table->unsignedBigInteger('client_id')->comment('顧客ID'); 20 $table->unsignedBigInteger('tax_id')->comment('税率ID'); 21 $table->string('estimate_number')->nullable()->comment('見積番号'); 22 $table->string('title')->comment('件名'); 23 $table->date('publish_dt')->comment('発行年月日'); 24 $table->date('expiration_dt')->nullable()->comment('見積有効期限'); 25 $table->string('condition')->nullable()->comment('支払条件'); 26 $table->string('extra', 1000)->nullable()->comment('備考'); 27 $table->timestamps(); 28 $table->softDeletes()->comment('削除日時'); 29 30 $table->foreign('company_id')->references('id')->on('companies'); 31 $table->foreign('client_id')->references('id')->on('clients'); 32 $table->foreign('tax_id')->references('id')->on('taxes'); 33 }); 34 } 35}

php

1<?php 2 3use Illuminate\Database\Migrations\Migration; 4use Illuminate\Database\Schema\Blueprint; 5use Illuminate\Support\Facades\Schema; 6 7class CreateTaxesTable extends Migration 8{ 9 /** 10 * Run the migrations. 11 * 12 * @return void 13 */ 14 public function up() 15 { 16 Schema::create('taxes', function (Blueprint $table) { 17 $table->bigIncrements('id')->comment('ID'); 18 $table->unsignedTinyInteger('rate')->comment('税率'); 19 $table->date('start_dt')->comment('開始年月日'); 20 $table->date('end_dt')->nullable()->comment('終了年月日'); 21 $table->timestamps(); 22 $table->softDeletes()->comment('削除日時'); 23 }); 24 } 25}

ここで、Estimate と Tax において、素のSQLであれば、estimates.tax_id を使わずとも、

sql

1SELECT * FROM estimates 2INNER JOIN taxes 3ON (estimates.publish_dt BETWEEN taxes.start_dt AND taxes.end_dt) 4 OR (estimates.publish_dt >= taxes.start_dt AND taxes.end_dt IS NULL)

と、リレーションを取ることが可能です。

コントローラーから見積書一覧を取得するときに、withを使ってアクセスしたいので、

php

1<?php 2 3namespace App\Http\Controllers; 4 5use App\Models\Estimate; 6 7class EstimateController extends Controller 8{ 9 public function index() 10 { 11 $estimates = Estimate::with(['tax']) 12 ->orderByDesc('updated_at') 13 ->orderByDesc('id') 14 ->paginate(); 15 return view('estimates.index', compact('estimates')); 16 } 17}

まず最初に、このSQLを表現するリレーションを表現できないのか?
と調べましたが、もちろん以下の形では表現できません。

php

1<?php 2 3use Illuminate\Database\Eloquent\Model; 4 5class Estimate extends Model 6{ 7 public function tax(): BelongsTo 8 { 9 return $this->belongsTo(Tax::class, 'publish_dt'); // NG 10 } 11}

また、以下のように表現しても、できなくはないですが、
一覧表示するときにループの中で毎回SQLを発行してしまうので、良いアプローチではないと思います。

php

1<?php 2 3use Illuminate\Database\Eloquent\Model; 4 5class Estimate extends Model 6{ 7 public function tax() 8 { 9 return Tax::whereDate('start_dt', '<=', $this->publish_dt) 10 ->where(function($query){ 11 $query->whereDate('end_dt', '>=', $this->publish_dt) 12 ->orWhereNull('end_dt') 13 })->first(); 14 } 15}

暫定的に採用したアプローチ

estimatestax_id を追加し、素直にbelongsTo でリレーションし、
EstimateObserver クラスで creating , updating を定義しています。

php

1<?php 2 3namespace App\Observers; 4 5use App\Models\Estimate; 6use App\Models\Tax; 7 8class EstimateObserver 9{ 10 public function creating(Estimate $estimate) 11 { 12 if (is_null($estimate->publish_dt)) { 13 return; 14 } 15 $publish_dt = $estimate->publish_dt; 16 $estimate->tax_id = Tax::whereDate('start_dt', '<=', $publish_dt) 17 ->where(function ($query) use ($publish_dt) { 18 $query->whereNull('end_dt') 19 ->orWhereDate('end_dt', '>', $publish_dt); 20 })->first()->id; 21 } 22 23 public function updating(Estimate $estimate) 24 { 25 if (is_null($estimate->publish_dt)) { 26 return; 27 } 28 $publish_dt = $estimate->publish_dt; 29 $estimate->tax_id = Tax::whereDate('start_dt', '<=', $publish_dt) 30 ->where(function ($query) use ($publish_dt) { 31 $query->whereNull('end_dt') 32 ->orWhereDate('end_dt', '>', $publish_dt); 33 })->first()->id; 34 } 35}
tanat, mpyw, SoulaS👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

m.ts10806

2019/04/13 09:06

回答依頼いただいておいて申し訳ないのですが、 まだ「職人」というほど使いこなせてないですし(5系は業務未経験)、 Kosuke_Shibuyaさんが出されている以上のアプローチは現状出てこないですね・・すみません
xenbeat

2019/04/16 13:43

最終的には「ご自身で採用したアプローチ」をやめて「ベストアンサーのアプローチ」をそのまま採用されたという理解でよろしいでしょうか? 色んなトレードオフの中で最終的にKosuke_Shibuyaさんがこの問題に対して出した答え(解決した方法)を共有いただけますでしょうか。
guest

回答3

0

ベストアンサー

あー,これ普通にリレーション自作案件ですね。「トリッキーなことはしたくない」というのは重々承知ですが,Laravel 本家が不等号を使った比較に対応していない以上リレーションを自作するのはやむを得ないと思います。再利用性を高くしたかったら Relation 継承クラスを返り値にしないといけないですし。

【Laravel】 第2回 Eloquent ソースコードリーディング - リレーションの取得 - Qiita

php

1class HasTax extends Relation 2{ 3 protected $model; 4 protected $dateColumn; 5 6 public function __construct(Model $model, string $dateColumn) 7 { 8 $this->model = $model; 9 $this->dateColumn = $dateColumn; 10 } 11 12 protected function getDate(Model $model) 13 { 14 return $model->{$this->dateColumn}; 15 } 16 17 public function addConstraints() 18 { 19 if (static::$constraints) { 20 $this->query->whereDate('start_dt', '<=', $this->getDate($this->model)); 21 $this->query->where(function (Builder $query) { 22 $query 23 ->whereDate('end_dt', '>=', $this->getDate($this->model)) 24 ->orWhereNull('end_dt'); 25 }); 26 } 27 } 28 29 public function addEagerConstraints(array $models) 30 { 31 $this->query->where(function (Builder $query) use ($models) { 32 foreach ($models as $model) { 33 $query->orWhere(function (Builder $query) use ($model) { 34 $query->whereDate('start_dt', '<=', $this->getDate($model)); 35 $query->where(function (Builder $query) { 36 $query 37 ->whereDate('end_dt', '>=', $this->getDate($model)) 38 ->orWhereNull('end_dt'); 39 }); 40 }); 41 } 42 }); 43 } 44 45 public function initRelation(array $models, $relation) 46 { 47 foreach ($models as $model) { 48 $model->setRelation($relation, null); 49 } 50 return $models; 51 } 52 53 public function match(array $models, Collection $taxes, $relation) 54 { 55 foreach ($models as $model) { 56 $date = $model->{$this->dateColumn}; 57 58 foreach ($taxes as $tax) { 59 if ($tax->start_dt <= $date && ($date <= $tax->end_dt || $tax->end_dt === null)) { 60 $model->setRelation($relattion, $tax); 61 break; 62 } 63 } 64 } 65 66 return $models; 67 } 68 69 public function getResults() 70 { 71 return $this->query->first(); 72 } 73}

これでモデル側で

php

1return new HasTax($this, 'publish_dt');

すればリレーション定義完了です。hasMany hasOne belongsTo にならって hasTax を定義しておくと更にベター。

投稿2019/04/14 09:27

編集2019/04/14 09:47
mpyw

総合スコア5223

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mpyw

2019/04/14 09:31

動作未検証なので細かいミスはご容赦ください
mpyw

2019/04/14 09:34 編集

<del>あ… getEager() も定義しないといけないな。修正します</del> <ins>そんなことはなかった</ins>
mpyw

2019/04/14 09:39

match メソッドで,foreach の中で foreach 回してるのが若干嫌な気もしますが,内側の foreach はほぼ無視できる程度のオーバーヘッドなので気にする必要はないでしょう。消費税そんなに変わってないのでww
退会済みユーザー

退会済みユーザー

2019/04/14 16:04

回答ありがとうございます。 あー、なるほどと感心しました。 HasTaxクラスをどこに書くのがベストプラクティスだろうかと模索中です。 採用するかどうかは未定ですが、実際にやってみて判断します。 artisan vendor:publish でasset のようにオーバーライドできれば採用!なんですが、後々のメンテナンス性、学習コストとトレードオフかなあ。
mpyw

2019/04/14 17:52

app/Models ディレクトリを切っているのであれば ・app/Models/Relations 無いのであれば ・app/Relations ・app/Database/Eloquent/Relations あたりが候補ですかね。
mpyw

2019/04/14 17:53

確かに学習コストはだいぶ上がってしまいますね。しかし1回これを書いてしまえば他のところはものすごくきれいになるので、メリットもあります。トレードオフなので、出来るだけいい落とし所を探してください。
mpyw

2019/04/14 17:54

【Laravel】 Eager Loading でネストしたリレーションをカウントする - Qiita https://qiita.com/mpyw/items/ccbffdd07330fd9f1c39 (↑正直これとかやりすぎ感あるけど、リレーション定義頑張ったおかげで今も商用環境でカウンターキャッシュを一切使わずとも爆速でレスポンスが返っているのでやって悪かったとは思ってない)
guest

0

検討されている仕様が汲み取れていないかもしれないですし、そのようなシステムの開発経験がないため色々と考慮漏れがあるかもしれませんが、個人的な考え方が少しでも参考になればと思い恐縮ながら回答させていただきます。

見積モデルの「見積書発行日」時点での消費税モデルをどうリレーションするのか?

Laravelというより設計の話になりますが、そもそもリレーションする必要がないと思っています。

自分が採用したアプローチの他にもっと良い方法はないのか?

私ならtax_idの代わりにtax_rateのようなカラムを持ち、作成・更新時点の税率をそのまま記録すると思います。EstimateObserverはそのまま残しても良いと思いますが、作成時と更新時で次のロジックが重複するので

PHP

1$estimate->tax_id = Tax::whereDate('start_dt', '<=', $publish_dt) 2 ->where(function ($query) use ($publish_dt) { 3 $query->whereNull('end_dt') 4 ->orWhereDate('end_dt', '>', $publish_dt); 5 })->first()->id;

次のようにTaxのモデルを作って、そこに上記と同様の税率取得ロジックを閉じ込めちゃって、estimatesの作成時と更新時にそのメソッドの戻り値を$estimate->tax_rateに設定すれば良いと思います。

PHP

1class Tax extends Model 2{ 3 public function 税率を取得($date=現在日): int 4 { 5 ... 6 } 7}

あとは余談ですが私ならestimatesstatusカラムも持つと思います。
発行前までは「下書き」として保持して、発行後に「発行済み」に更新、その後一切更新できないようにします。
もし「発行済み」の後に内容を更新したい場合は「破棄」して再作成させるようにします。
「発行済み」の見積書は相手に渡ったりして、業務上の重要な記録となるので、その業務記録が改ざん出来ないように設計します。
リレーションは必要ないというのも上記の考えに基づくものです。

投稿2019/04/13 12:59

編集2019/04/13 13:25
xenbeat

総合スコア4258

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2019/04/13 14:19

回答ありがとうございます. ご提案の方法だと、estimates テーブルの tax_rate に0, 3, 5, 8, 10 以外の不正なデータが入り込む余地ができてしまいますね。7というデータが入らないようにする別のロジックが必要になったり、不正データがないことを担保する別の仕組みが必要になりそうです。
xenbeat

2019/04/13 15:20 編集

トリッキーな方法でリレーションを貼ったとしても、POST時にはデータベース側(外部キー制約は使えないですよね?)ではなく、どのみちアプリケーション側でデータベースに問い合わせるなりして例外処理を適切にすべきかと思いますがいかがでしょうか。 その処理含めTaxのモデルに税率取得処理を切り出すというご提案です。
退会済みユーザー

退会済みユーザー

2019/04/13 14:41 編集

> 外部キー制約は使えないですよね? 質問文では割愛しましたが、使ってますよ。
xenbeat

2019/04/13 14:42

estimates.tax_id = tax.idという制約ですよね?
退会済みユーザー

退会済みユーザー

2019/04/13 14:43 編集

そうです。そうです。 「暫定的なアプローチ」で実装している方法では、です。
xenbeat

2019/04/13 15:05

ですよね。 ただその制約だけでは不十分だと(データベース側だけで担保するのは難しい)思うのですが。 念のため確認ですが、見積書作成/更新時の税率はユーザー入力ではなくシステムで「正しい税率(そのときに適用されるべき税率)」を自動設定という認識でよろしいでしょうか? EstimateObserverの「creating」と「updating」でtax_idを設定されているのと、質問に記載されている次の要件からそのような認識でいるのですが。 > 発行日が変更されたとき、正しい消費税率を関連づけたい。できれば発行日が変更されても「よしなに」税率も変更される。
退会済みユーザー

退会済みユーザー

2019/04/13 15:07

発行日はユーザー入力で、税率はシステムでの自動設定となります。
xenbeat

2019/04/13 16:01 編集

> 発行日はユーザー入力で、税率はシステムでの自動設定となります その認識は合ってそうです。 tax_idでもtax_rateでも、ユーザー入力の「発行日」から「正しい税率(そのときに適用されるべき税率)」を導き出すのはアプリケーションのロジックですよね。 データベースの制約(estimates.tax_id = tax.id)では税率が「(例)5, 8, 10」の中のどれかであるというのが担保できるだけだと思うので、それを含め正しい税率(そのときに適用されるべき税率)かどうかというのはアプリケーション側のロジック、テストで担保するのが良いと思った次第です。 で、そのロジックをTaxというモデルに書いたらどうかという提案です。 否定しているわけではなく、あくまでも私ならこうするかもという話なので参考程度に捉えていただければ幸いです。
guest

0

リレーション(belongsToやhasMany)にwhere文をくっつける事ができます。

php

1return $this->belongsTo(Tax::class)->whereDate('start_dt', '<=', $this->publish_dt) 2 ->where(function($query){ 3 $query->whereDate('end_dt', '>=', $this->publish_dt) 4 ->orWhereNull('end_dt') 5 });

こんな感じで対応できないでしょうか?な、withできるかは不明です

投稿2019/04/13 09:32

mikkame

総合スコア5036

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2019/04/13 14:01 編集

回答ありがとうございます。 いただいた方法では、なくても良いはずの tax_id が必要になり、またpublish_dt 変更時にtax_id を変更する別の手段が発生しますね。 実際に試したところ、eager ロードは使えず,、with を使用することができませんでした。 $this->publish_dt が null になってしまうためです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問