57

I'm trying to update Model which has two primary keys.

Model

namespace App;

use Illuminate\Database\Eloquent\Model;

class Inventory extends Model
{
    /**
     * The table associated with the model.
     */
    protected $table = 'inventories';

    /**
     * Indicates model primary keys.
     */
    protected $primaryKey = ['user_id', 'stock_id'];
...

Migration

Schema::create('inventories', function (Blueprint $table) {
    $table->integer('user_id')->unsigned();
    $table->integer('stock_id')->unsigned();
    $table->bigInteger('quantity');

    $table->primary(['user_id', 'stock_id']);

    $table->foreign('user_id')->references('id')->on('users')
        ->onUpdate('restrict')
        ->onDelete('cascade');
    $table->foreign('stock_id')->references('id')->on('stocks')
        ->onUpdate('restrict')
        ->onDelete('cascade');
});

This is code which should update Inventory model, but it doesn't.

$inventory = Inventory::where('user_id', $user->id)->where('stock_id', $order->stock->id)->first();
$inventory->quantity += $order->quantity;
$inventory->save();

I get this error:

Illegal offset type

I also tried to use updateOrCreate() method. It doesn't work (I get same error).

Can anyone tell how Model with two primary key should be updated?

alepeino
  • 9,551
  • 3
  • 28
  • 48
Ugnius Malūkas
  • 2,649
  • 7
  • 29
  • 42
  • Illegal offset type, which type? has a line number? exists a inventory in this query? – marcosrjjunior Mar 31 '16 at 11:38
  • ErrorException in laravel/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php line 2762. ` /** * Get the casts array. * * @return array */ public function getCasts() { if ($this->getIncrementing()) { return array_merge([ $this->getKeyName() => 'int', // line 2762 ], $this->casts); } return $this->casts; } ` By the way, this inventory exists. – Ugnius Malūkas Mar 31 '16 at 11:41
  • Paste in the full error so we can see the entire message in context. – Niraj Shah Mar 31 '16 at 11:59
  • 1
    Check this https://github.com/laravel/framework/issues/5517 Laravel eloquent does not support composite primary keys. (Sidenote, you can't have two primary keys. Both fields are part of the one and only primary key) – apokryfos Mar 31 '16 at 12:03
  • Full error http://pastebin.com/yaG0aZYM – Ugnius Malūkas Mar 31 '16 at 12:06

1 Answers1

146

I've run into this problem a couple of times. You need to override some properties:

protected $primaryKey = ['user_id', 'stock_id'];
public $incrementing = false;

and methods (credit):

/**
 * Set the keys for a save update query.
 *
 * @param  \Illuminate\Database\Eloquent\Builder  $query
 * @return \Illuminate\Database\Eloquent\Builder
 */
protected function setKeysForSaveQuery(Builder $query)
{
    $keys = $this->getKeyName();
    if(!is_array($keys)){
        return parent::setKeysForSaveQuery($query);
    }

    foreach($keys as $keyName){
        $query->where($keyName, '=', $this->getKeyForSaveQuery($keyName));
    }

    return $query;
}

/**
 * Get the primary key value for a save query.
 *
 * @param mixed $keyName
 * @return mixed
 */
protected function getKeyForSaveQuery($keyName = null)
{
    if(is_null($keyName)){
        $keyName = $this->getKeyName();
    }

    if (isset($this->original[$keyName])) {
        return $this->original[$keyName];
    }

    return $this->getAttribute($keyName);
}

Remember this code needs to reference Eloquent Builder class with

use Illuminate\Database\Eloquent\Builder;

I suggest putting those methods in a HasCompositePrimaryKey Trait so you can just use it in any of your models that need it.

alepeino
  • 9,551
  • 3
  • 28
  • 48
  • 3
    awsm. thanks! it really should be supported in the base framework tho.... – bhu Boue vidya Apr 21 '17 at 08:10
  • yeah, I think it should. But I doubt it will ever happen – alepeino Apr 21 '17 at 12:43
  • 4
    I've made a trait with this code, now I only need to `use CompositeKeyModelHelper;` on my Models. Thansk! – António Almeida May 24 '17 at 09:46
  • 1
    Added this to a model and it is failing. It appears something is calling getKey() in Illuminate\Database\Eloquent\Model before it calls either of the functions listed in this example, resulting in an error to be thrown since it isn't expecting an array for the attributes. – Jake Boomgaarden Aug 18 '17 at 10:16
  • Do I need to call the methods `setKeysForSaveQuery` manually? Or are these methods only to tell the framework what to do in the background when calling `Mode_instance->save()`? – Adam Oct 13 '17 at 12:33
  • @Adam no, just override the methods, they will be used internally during the insert/update operations of the model – alepeino Oct 13 '17 at 13:36
  • @Adam I think I haven't yet used this on 5.5, but you say `App\Lookup\Buil‌​der`... Maybe you are referencing a different `Builder` in the `use` statement? – alepeino Oct 13 '17 at 18:29
  • 4
    Okay your right, I *was* using a differnet builder! I corrected it but its still not working: `Declaration of App\Lookup\CountryStats::setKeysForSaveQuery(Illu‌​minate\Database\Eloq‌​uent\Builder $query) should be compatible with Illuminate\Database\Eloquent\Model::setKeysForSaveQuery(Illuminate\Database\Eloquent\Builder $query)`. I don't understand why because it has same parameter and visibility as the method in `Model`. I also noticed that `setKeysForSaveQuery` has no input parameter in `Model`. – Adam Oct 13 '17 at 18:43
  • @Adam you mean `getKeyForSaveQuery`? Yes, that shouldn't be a confilct but maybe there's no need to override that method. I think `$query->where($keyName, '=', $this->original[$keyName] ?? $this->getAttribute($keyName));` instead in the set method should work. I have not tried this though – alepeino Oct 13 '17 at 21:01
  • @Adam On the other hand, I just tried this method as is and I have no declaration conflicts. It's weird that you do, since they are both the same method signature. – alepeino Oct 13 '17 at 21:03
  • I'm getting the ` Declaration of App\Models\Traits\HasCompositePrimaryKey::setKeysForSaveQuery(App\Models\Traits\Builder $query) should be compatible with Illuminate\Database\Eloquent\Model::setKeysForSaveQuery(Illuminate\Database\Eloquent\Builder $query) ` – Lewis Feb 05 '18 at 11:34
  • I'm getting the same error in Laravel 5.5 – Hirad Roshandel Mar 05 '18 at 17:51
  • How would you go about doing this when your primary keys are id and year, where id should be incremented on add item? Also is this working with find()? Can I use ::find($id, $year)? – orbitory Jun 11 '18 at 01:31
  • 2
    In case someone that used this solution is using soft deletes. You'll need to overwrite runSoftDelete too https://pastebin.com/d3ED089R – Gabo Jul 31 '18 at 21:24
  • 1
    Folks who are getting incompatibility errors need to add the following to their use statements in their model: `use \Illuminate\Database\Eloquent\Builder;` because if you don't, it will look for `Builder` in the model's namespace (`App\Builder`) and not Eloquent's as it should be. @alepeino consider updating your answer include this into it as this question gets a lot of activity. – Script47 Jul 04 '19 at 14:28
  • 1
    this is not working on laravel 8 ... – Alberto Acuña Dec 08 '20 at 12:22
  • 1
    @AlbertoAcuña do `protected function setKeysForSaveQuery($query)` insead of `protected function setKeysForSaveQuery(Builder $query)` – iateadonut Nov 03 '21 at 05:48
  • Why not create a separate `id` key and keep the other two keep the combined unique key? anyone can please explain? – Vishal Tarkar Aug 28 '23 at 09:02