Asked  7 Months ago    Answers:  5   Viewed   47 times

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

Model

namespace App;

use IlluminateDatabaseEloquentModel;

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?

 Answers

36

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  IlluminateDatabaseEloquentBuilder  $query
 * @return IlluminateDatabaseEloquentBuilder
 */
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 IlluminateDatabaseEloquentBuilder;

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

Wednesday, March 31, 2021
 
OMGKurtNilsen
answered 7 Months ago
72

We have now solved this generically for all models by extending our base model with the following functionaly:

  • We define an array of attributes that hold geometric data.
  • We decide on a per-model-basis if we want this to be auto-loaded as text.
  • We change the default query builder to select the geometry attributes as text from the database.

Here is an excerpt from the base model we now use:

/**
 * The attributes that hold geometrical data.
 *
 * @var array
 */
protected $geometry = array();

/**
 * Select geometrical attributes as text from database.
 *
 * @var bool
 */
protected $geometryAsText = false;

/**
 * Get a new query builder for the model's table.
 * Manipulate in case we need to convert geometrical fields to text.
 *
 * @param  bool  $excludeDeleted
 * @return IlluminateDatabaseEloquentBuilder
 */
public function newQuery($excludeDeleted = true)
{
    if (!empty($this->geometry) && $this->geometryAsText === true)
    {
        $raw = '';
        foreach ($this->geometry as $column)
        {
            $raw .= 'AsText(`' . $this->table . '`.`' . $column . '`) as `' . $column . '`, ';
        }
        $raw = substr($raw, 0, -2);
        return parent::newQuery($excludeDeleted)->addSelect('*', DB::raw($raw));
    }
    return parent::newQuery($excludeDeleted);
}
Wednesday, March 31, 2021
 
tika
answered 7 Months ago
31

This can be done in (at least) 2 ways.

Using pure Eloquent model logic:

class Buy extends Model
{
  public function getTotalPrice() {
    return $this->buyDetails->sum(function($buyDetail) {
      return $buyDetail->quantity * $buyDetail->price;
    });
  }
}

The only issue here is that it needs to fetch all buy details from the database but this is something you need to fetch anyway to display details in the view.

If you wanted to avoid fetching the relation from the database you could build the query manually:

class Buy extends Model
{
  public function getTotalPrice() {
    return $this->buyDetails()->sum(DB::raw('quantity * price'));
  }
}
Saturday, May 29, 2021
 
Jeff
answered 5 Months ago
19

I got the problem solve turns out that my factory method in my test is using

factory(User::class)->make() 

it should be

factory(User::class)->create()
Saturday, May 29, 2021
 
Claudio
answered 5 Months ago
92

I corrected the code below pluck('cust') to pluck('cust_name') and pluck('cust_no') to pluck('cust_code') and it works

DB::table('delivery_sap')
    ->whereNotIn('cust', DB::table('customer')->pluck('cust_name'))
    ->whereNotIn('cust_no', DB::table('customer')->pluck('cust_code'))
    ->select('cust', 'cust_no')
    ->groupBy('cust', 'cust_no')
    ->get();
Wednesday, August 11, 2021
 
cyber_truite
answered 3 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :