Framework: Model::create() misses Database-Default-Values

Created on 29 Sep 2017  路  10Comments  路  Source: laravel/framework

  • Laravel Version: 5.5.x
  • PHP Version: 7.1
  • Database Driver & Version:

Dear Laravel Dev-Team and community,

I recently stumbled upon an issue when creating a new model that has some default values set in the migration file. This causes clients that interact with the application to retrieve "wrong" (incomplete) data, which is - at least in my opinion - not desired behavior.

Description:

If i insert a new Model to the database, the default values from the database are correctly set (in the database), the Model, however, do not have them.

Example:

Consider the following example, that illustrates the issue in more detail:
I have a projects Table that holds all Projects in my web application. A Project may be flagged as is_private (boolean) to indicate if it is accessible by everyone (false) or not (true).

The database migration for this table looks like this (shortened)

$table->string('name');
$table->text('description');
$table->boolean('is_private')->default(false);

So the is_private field is optional with a default value false on the database. This is created correctly and works like a charm.

Now, I am making a Request to my API in order to create a new Project. The Request body looks like this:

{
   "data" : {
      "name" : "My Project",
      "description" : "This is a fancy description for the project"
   }
}

So there is no is_private attribute in the JSON Request body. Next, after sanitizing the request data I do a:

$project = Project::create($data); // where $data is all data from the request

As the is_private field is not passed (which is completely fine), this is not present in the $data array. If I then return the $project it is missing the is_private attribute (was it was never set!). When i return this to the client (e.g., by using a Transformer like league/fractal) this would result in

is_private = null // because it was not set in the request

instead of

is_private = false // this is the default value according to the database-migration that is set after creating the model

Steps To Reproduce:

Create a completely new Laravel application, add a new (optional) field to the users table and set a default value for this. Then use php artisan tinker to create a new User with

$user = User::create(['name' => 'a', 'email' => '[email protected]', 'password' => 'test']);

Possible Solutions:

At the moment, I see 2 possible ways to solve this issue, however, both have different issues:

Refresh the Model after CREATE

When creating the model it must be immediately refreshed before returning. This would cause Eloquent to reload / refresh the entire model. Then, the "missing" attributes (which have default values on DB level) will be added.

$project = Project::create($data);
$project = $project->refresh();
return $project;

However, this results in an additional database-query to retrieve the newly created model..

Set Default Values on Model Level

I know that there is the possibility to set some kind of "default values" on the Model level by using

protected $attributes = [
   'is_private' = false;
];

This would cause Eloquent to add these attributes to a newly created model and then overwrite them with values that are passed to the ::create() method.
However, this way you would define "default values" on the database-layer and on the model layer - which is not convenient, I think.

Question

Is this a bug or is this a "feature"? If it is a "feature", how shall I tell Laravel to use the "correct" (depending on the point of view!) behavior. I define _correct_ in that way, that a ::create() also returns the default values from the database - because the Model that was created does, obviously, have those values set!

Cheers and thanks a lot for your reply!
Johannes

Most helpful comment

@Kyslik yes, agree - but you will need to maintain this on 2 different "sides".. The database and model level.. This means, in return, that you don't have a "single point of truth"...

All 10 comments

There's really no other way to tell your code about something that happened at the database level, but to hit the db again. Otherwise, you have to tell the model beforehand about your default fields and values, just as you mentioned above. This is the way it's designed and has been discussed many times here.

yes for these cases you have the choice to refresh the model and have this extra DB query, but we can't force it in the core since it's not always needed.

@themsaid would adding an additional autorefresh ( = false) param for the create() be an option?

@devcircus yes, but in case of really creating a new model it is quite obvious, that I would like to have the correct version of the object - and not a "half-filled" version.. So - at least for create - the current approach is not sufficient..

In return, this means, that there is absolutely no use-case to use ->default() in migrations, because you will need to implement it on Model level as well..

@johannesschobel you can always do that yourself, overload Laravel's create method and use what you've proposed (send $refresh boolean to the method as well).

->default() has its use-case even if model does not refreshes itself after creation, because user (of Laravel) mentally knows that such and such attribute is set to "default" value by database engine.

@Kyslik yes, agree - but you will need to maintain this on 2 different "sides".. The database and model level.. This means, in return, that you don't have a "single point of truth"...

This is a design flaw / bug. How would you explain conceptually the object returned by create? It is not a useful object, so there is no sensible term express it.

the answer here is to refresh the model after create if you need it.

There's another way: you can define protected $attributes = [鈥; on your model, containing the default values.

Downside: you've to manually keep them in sync with the actual DB defaults but YMMV if you prefer this over having to call ->fresh() or something.

If your logic ends up returning a Response, you can return a redirect instead of a view after you created an instance that should have default values.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

PhiloNL picture PhiloNL  路  3Comments

jackmu95 picture jackmu95  路  3Comments

RomainSauvaire picture RomainSauvaire  路  3Comments

klimentLambevski picture klimentLambevski  路  3Comments

SachinAgarwal1337 picture SachinAgarwal1337  路  3Comments