Autoincrement column with conditions - laravel

I would like to implement an order column (that will be used for determining hierarchy for the entities, both in backend and frontend). Is there a nice way to do this on the migration level?
So that when a new model is saved, the highest number of the order column will be found - but only for the models that share shop_id. So if I add models like this:
// First model should get order = 1
$priceExceptionA = new PriceException();
$priceExceptionA->shop_id = 1;
$priceExceptionA->amount = 100;
$priceExceptionA->save();
// Second model should get order = 2
$priceExceptionB = new PriceException();
$priceExceptionB->shop_id = 1;
$priceExceptionB->amount = 100;
$priceExceptionB->save();
I would like to find a way to implement this behavior already through the migration:
public function up()
{
Schema::create('price_exceptions', function (Blueprint $table) {
$table->id();
$table->integer('shop_id')->unsigned();
$table->integer('amount')->nullable();
// Find the highest order value for rows with the same shop_id and increment this by one
$table->integer('order')->default(////)
$table->timestamps();
$table->softDeletes();
$table->foreign('shop_id')->references('id')->on('shops')->onDelete('cascade');
});
}

I do not believe SQL can what you want to do with the default functionality.
Instead there is a simple solution using model events, that secures all models will get these order values. The creating events, is called before the model is saved to the database and therefor all models will get the order set.
public static function boot()
{
static::creating(function(PriceException $priceException)
{
$priceException->order = ($priceException->shop->priceExceptions()->max('order') ?? 0) + 1;
});
}
This can create race conditions a way to combat this is to use a lock to secure you don't get duplicate order numbering. If you work on a load balanced setup the cache should be a shared redis server.
static::creating(function(PriceException $priceException)
{
Cache::lock($priceException->shop_id)->get(function () use ($priceException) {
$priceException->order = ($priceException->shop->priceExceptions()->max('order') ?? 0) + 1;
});
});

Related

Laravel multiple relation in a same table

In my project, users can like & comment on feeds & forums. So, there is a contribution page where the user can see where he has provided his input (like or comment) sorted by created_at time.
There may be another feature in future like feed & forum where user can also provide like & comment.
In my contribution page, I want to list data like this -
You have commented on user_2's feed feed_title at created_at - comment
You have liked user_2's feed feed_title at created_at
You have commented on user_3's forum forum_title at created_at - comment
You have liked user_3's forum forum_title at created_at
But I am stuck in database design. So far I am trying this -
Schema::create('contributions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->uuid('user_id');
$table->uuid('contribution_id'); // id of my like/comment
$table->string('contribution_type'); // feed_like/feed_comment/forum_like/forum_comment
$table->uuid('target_id'); // id of feed/forum where I provided comment or like
$table->timestamps();
});
But it will cause a query loop when I retrieve the data. So, is there any better approach to what I am trying to get?
You are probably looking for Polymorphic Relationships.
That enables you to simplify the relationship by providing an ID of the related model and a naming of the related model instead.
A sample migration would look like this, using the morph method as inspiration (since you're using UUID's):
Schema::create('contributions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->uuid('user_id');
$table->uuid('contributable_id');
$table->string('contributable_type');
$table->timestamps();
});
This should enable you to do something like:
class Contribution extends Model {
public function contributable() {
return $this->morphTo();
}
}
class User extends Model
{
/**
* Get the user's contributions.
*/
public function contributions()
{
return $this->morphToMany(Contribution::class, 'contributable');
}
}
You should be able to retrieve the users contributions that way and defining the action based on the morphed instance type.

Calculated fields in database - Laravel

What is the best practice to store calculated fields in database.
For example, lets say a table has fields height, weight, bmi
A user enters height weight values and bmi field is automatically filled. How to achieve this with a form.
Formula for bmi
$bmi = weight / (height * height)
Tried the following
Profile Model
protected $table = 'profiles';
protected $fillable = ['user_id', 'weight', 'height', 'dob', 'age', 'bmi'];
public function user(){
return $this->belongsTo(User::class, 'user_id');
}
protected static function boot() {
parent::boot();
static::saving(function($model){
$model->bmi = $model->weight / ($model->height * $model->height);
$model->age = (date('Y') - date('Y',strtotime($model->dob)));
});
}
Profile Controller
public function store(Request $request)
{
$profile = new Profile();
$profile->weight = $request->get('weight');
$profile->height = $request->get('height');
$profile->dob = $request->get('dob');
$profile->age;
$profile->bmi;
$profile->save();
return back()->with('success', 'Your profile has been updated.');
}
But im receiving an error
Illuminate \ Database \ QueryException (42S22)
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'weight' in 'field list' (SQL: insert into `users` (`weight`, `height`, `dob`, `bmi`, `age`, `created_by`, `updated_by`, `updated_at`, `created_at`) values (60, 175, 1988-04-03, 0.0019591836734694, 30, 2, 2, 2018-03-08 20:06:02, 2018-03-08 20:06:02))
You could do this in the boot method of the model:
protected static function boot() {
parent::boot();
static::saving(function($model){
$model->bmi = $model->weight / ($model->height * $model->height);
});
}
What is the best practice to store calculated fields in database.
In general, don't. It's redundant - your database already has all the information needed to compute it, so why store redundant data?
Instead, put an accessor on the model:
public function getBmiAttribute() {
return ($this->weight / ($this->height * $this->height));
}
You can then do $this->bmi to get the computed value.
If you must have it in the database, use an Eloquent event. You'd probably want to hook into the saving event.
As of Laravel 5.3 & MySQL 5.7, you can use Column Modifiers virtualAs and storedAs to create a VIRTUAL (evaluated when rows are read) or STORED (evaluated and stored when rows are inserted or updated) generated column for MySQL.
I've made use of virtualAs in the example below...
Schema::create('results', function (Blueprint $table) {
$table->bigIncrements('id');
...
$table->time('predicted_time')->nullable()->default(NULL);
$table->time('handicap')->nullable()->default(NULL);
$table->time('finish_time')->nullable()->default(NULL);
$table->time('actual_time')->virtualAs('TIMEDIFF(finish_time, handicap)');
$table->time('offset')->virtualAs('TIMEDIFF(actual_time, predicted_time)');
...
});
What is the best practice to store calculated fields in database.
It depends on your use case. If you're using a relational database, and your use case does not involve big data (in terms of volume, variety or velocity), the best practice is to not store calculated fields and calculate them on the fly.
If you're using a noSQL database (such as MongoDB, which is supported by Laravel) or Hadoop, the best practice is to store calculated fields to avoid computational time.
In A Nutshell
It's a tradeoff between time complexity and space/storage complexity.
For big data / noSQL systems, store calculated fields especially if
they are computationally complex. For a relational database, calculate
them on the fly and keep your data non-redundant
Laravel Solution for RDBMS:
Use accessors like so:
public function getBmiAttribute($value)
{
return ($this->weight / ($this->height * $this->height));
}
Laravel Solution for NoSql
Use model events like so:
protected static function boot() {
parent::boot();
static::saving(function($model){
$model->bmi = $model->weight / ($model->height * $model->height);
});
}
If your DBMS supports computed columns (aka generated columns), you might consider utilizing them.
Highlights:
Computed columns don't persist the data to the database (or if they do technically they'll be managed by the DBMS), so you're not duplicating any data or making anything redundant
The calculation is then available outside of the application's code base for anything to use. For example, if there is a need to develop reporting with raw SQL queries then the calculation will be available there.
Eloquent (Laravel's default ORM) will pick up the column without you necessarily needing to define it anywhere.
You could execute code to create it during a migration. For example, I had a use case where I wanted to simplify determining if something was currently published and I did it like this:
Schema::table('article', function (Blueprint $table) {
$table->dateTime('published_at')->nullable();
$table->dateTime('unpublished_at')->nullable();
});
$sql = "ALTER TABLE article ADD is_published AS CAST(CASE WHEN GETUTCDATE() BETWEEN ISNULL(published_at, '2050-01-01') AND ISNULL(unpublished_at, '2050-01-01') THEN 1 ELSE 0 END AS BIT);";
DB::connection()->getPdo()->exec($sql);
Then after retrieving a record from the database I can determine if it's published by simply checking...
if ($article->is_published) ...
And if I want to query it from the database I can easily do...
SELECT * FROM article WHERE is_published = 1
Note, this syntax is for T-SQL since the RDBMS for my project is MS SQL Server. However, other popular DBMS have similar features.
MS SQL Server: Computed Columns
ALTER TABLE dbo.Products ADD RetailValue AS (QtyAvailable * UnitPrice * 1.35);
MySQL: Generated Columns
ALTER TABLE t1 ADD COLUMN c2 INT GENERATED ALWAYS AS (c1 + 1) STORED;
Oracle: Virtual Columns
ALTER TABLE emp2 ADD (income AS (salary + (salary*commission_pct)));
PostgreSQL doesn't seem to support it (yet), but this SO answer provides a decent workaround.
another way to make this
making a computed column in the migration
this working with all DB:-
$table->computed ('tax','price * 0.27');
but this working with MySQL DB:-
$table->integer('tax')->virtualAs('price * 0.27')->nullable();
$table->integer('discount')->storedAs('price - 100')->nullable();

How to create a new many to many record with attach

Okey, i seen some posts about this but i don't understand the concept of attach at all, i have three tables:
Llistes(Lists):
$table->increments('id');
$table->string('nom_llista');
$table->integer('user_id')->unsigned();
});
Cancons(Songs):
$table->increments('id');
$table->string('titol');
$table->integer('genere_id')->unsigned();
$table->integer('artista_id')->unsigned();
$table->integer('album_id')->unsigned();
Pivot table: llistes_cancons (lists_songs):
$table->increments('id');
$table->integer('id_canco')->unsigned();
$table->integer('id_llista')->unsigned();
$table->timestamps();
I have two other Classes that i think that are correct, but i''m not sure:
In Canco.php (Song.php):
public function llistescancons_llistes()
{
return $this->belongsToMany('App\Llista');
}
In Llista.php (List.php):
public function llistescancons_cancons()
{
return $this->belongsToMany('App\Canco');
}
So, the question is how can I implement in my controller a function that let me add new record to the pivot table (many to many) and if it's possible another funtion to show the records, i'm newbie in Laravel and it's a bit hard for me.
There's no need to implement methods to add/remove records from the pivot table. Eloquent has attach/detach methods that can do that for you, but first you need to give Eloquent the column names of the pivot table since you are not using Eloquent's column name convention.
In Canco.php (Song.php):
public function llistescancons_llistes()
{
return $this->belongsToMany('App\Llista','llistes_cancons','id_canco','id_llista');
}
In Llista.php (List.php):
public function llistescancons_cancons()
{
return $this->belongsToMany('App\Canco','llistes_cancons','id_llista','id_canco');
}
Then if you want to attach a song to list, you can easily use the song id to do that
$list = App\Llista::find(1);
$list->llistescancons_cancons()->attach($songId);
or the other way around
$song = App\Canco::find(1);
$song->llistescancons_llistes()->attach($listId);

One to Many relation - how to implement push

I am trying to do an insert which will create a parent record for one table and then insert records that link back to the parent record into another.
In other words this: User completes Course information form, then completes a series of questions on the same page. On submission, the course information is inserted into its own table then questions are inserted into a separate one.
My Course model is this:
class CourseVerification extends Eloquent
{
public function courseverificationqanda()
{
return $this->hasMany('CourseVerificationQandA', 'id', 'verification_id');
}
My Question model is this:
class CourseVerificationQandA extends InnovedBaseModel
{
public function courseverification()
{
return $this->belongsTo('CourseVerification');
}
On form submission, my controller is doing this:
// create course verification record first
$veri = new CourseVerification;
$veri->verification_date = $input['verification_date'];
// create collection to store questions
$collection = new Illuminate\Database\Eloquent\Collection();
// loop through submitted questions and push them to the collection
for($i = 0; $i < count(Input::get('question_id')); $i++) {
$answer = new CourseVerificationQandA;
$answer->question_id = $input['question_id'][$i];
$answer->answer = $input['answer'][$i];
$answer->additional_notes = $input['additional_notes'][$i];
$collection->push($answer);
}
// add collection to quesetion relation
$veri->courseverificationqanda = $collection;
// insert both course record and questions
$veri->push();
The push method then errors and debugs the SQL command
insert into `CourseVerification`
(`verification_date`, `topic_id`, `course_id`, `verifier_id`,`iv_status`,
`verification_type`, `courseverificationqanda`, `created_by`)
values
(29/10/2014, 1294, 47, 1, 1, I, [{"question_id":"2","answer":"0","additional_notes":""},
{"question_id":"3","answer":"0","additional_notes":""},
{"question_id":"4","answer":"0","additional_notes":""}], 1)
As you can see, the assignment of the collection to $veri->courseverificationqanda is then getting treated as a table column in the SQL query when it is actually a relationship to the question table.
Any ideas?
You have a few mistakes there.
You don't assign collection to the relation. You need to load that relation (as a collection in this case) and push on it, then just save everything:
$veri = new CourseVerification;
$veri->verification_date = $input['verification_date'];
// Save the parent model before you can add its children
if ($veri->save())
{
// loop through submitted questions and push them to the collection
for($i = 0; $i < count(Input::get('question_id')); $i++) {
$answer = new CourseVerificationQandA;
// associate with the parent model
// explicitly
$answer->verification_id = $veri->getKey();
// or the eloquent way
// $answer->courseverification()->associate($veri);
$answer->question_id = $input['question_id'][$i];
$answer->answer = $input['answer'][$i];
$answer->additional_notes = $input['additional_notes'][$i];
$veri->courseverificationqanda->push($answer);
}
}
// save both course record and questions
$veri->push();
Another thing are your relations, which are both wrong:
// this is how it should look like
// CourseVerification
public function courseverificationqanda()
{
return $this->hasMany('CourseVerificationQandA', 'verification_id', 'id');
}
// QandA
public function courseverification()
{
return $this->belongsTo('CourseVerification', 'verification_id');
}
In firsts case you swapped the keys, so it would work but not the way it should.
In second case you didn't specify the foreign key at all, so Eloquent would look for courseverification_id in the table (method_name_id).

Creating a Many-to-many relationship in Laravel with additional data

I have in my database a pivot table that stores extra information. It has 2 foreign keys, and an additional field. Here's what it looks like:
EventTeam
int event_id (fk)
int team_id (fk)
boolean home
The intent here is that an Event may have many teams (in fact, it must have at least 2, but that's not a database constraint), and a team may participate in many events. However, for each event-team relationship, I want to also track whether the team is considered the home team for that event.
How do I define my model with this in mind? Do I have an EventTeam model at all, or do I define a belongsToMany relationship in both the Team and Event models? If I need a separate model, what relationships do I define in it? If I don't, how do I add the boolean field to the pivot table that gets used? I really have no idea how to do this.
You dont need a EventTeam model per se, but it could come in handy for seeders or if you are going to attach models to your EventTeam connection anywhere else in your app. This should work:
Event model:
public function teams()
{
return $this->belongsToMany('Team');
}
Team model:
public function events()
{
return $this->belongsToMany('Event');
}
For the extra boolean you can use ->withPivot().
$this->belongsToMany('Event')->withPivot('is_home');
See http://laravel.com/docs/eloquent#working-with-pivot-tables for more info.
Updated answers:
1) I would put it in both models so you can access the pivot data from both sides without a problem.
2) It should be to column name indeed.
3) Like i said its not really needed for you in this situation, but you could do this:
EventTeam model:
public function event()
{
return $this->belongsTo('Event');
}
public function team()
{
return $this->belongsTo('Team');
}
Add withPivot('home') on your relations definitions, then you can access it like this:
$team->events->first()->pivot->home; // 0/1
$event->teams->first()->pivot->home; // 0/1
first is just an example of getting single related model here.
Now, next thing is adding that value to the relation:
$team = Team::find($id);
$event = Event::find($eventId);
$team->events()->attach($event, ['home' => 1]);
// or
$team->events()->attach($eventId, ['home' => 1]);
// or using sync
$event->teams()->sync([1,5,15], ['home' => 0]);
Another thing is querying that field:
// load first team and related events, that the team hosts
$team = Team::with(['events'=>function ($q) {
$q->wherePivot('home', 1);
}])->first();
// load only teams that are hosts for any event
$hostTeams = Team::whereHas('events', function ($q) {
// wherePivot won't work here!
$q->where('event_team.home', 1);
})->get();
and so on.

Resources