abolfazlxyz's avatar

Pivot attributes are overriding relation attributes (Many to Many relation)

Hi everyone! Hope you doing well...

In a many to many relation between documents and transactions, I have a pivot model that has id and extra field amount along with relation columns transaction_id and document_id. You can see model definitions below, they're basic but notice that my document and transaction tables have their own amount column which are different than pivot's amount.

The problem is when I call $document->transactions or vice versa, it returns pivot's id and pivot's amount value instead of model id and amount [along with other attributes of the model].

Shouldn't I only get the models attributes and access the pivot attributes by calling $transaction->pivot? If I change pivot column names to something like pivot_id and pivot_amount everything is ok so...

Thanks in advance ;)

Here are the model definitions...

DocumentTransaction.php

class DocumentTransaction extends Pivot
{
    use SoftDeletes, AuthorStamps;
    //columns: id, transaction_id, document_id, amount, created_at, deleted_at, created_by, deleted_by,...

    public $incrementing = true;

    protected $table = 'document_transaction';
    protected $dates = ['deleted_at'];

    public function document(): Relation
    {
        return $this->belongsTo(Document::class);
    }

    public function transaction(): Relation
    {
        return $this->belongsTo(Transaction::class);
    }
}

Document.php

class Document extends Model
{
//columns: id, amount,...

public function transactions()
    {
        return $this->belongsToMany(Transaction::class)
            ->using(DocumentTransaction::class)
            ->wherePivot('deleted_at', null)
            ->withPivot(['amount'])
            ->withTimestamps();
    }
}

Transaction.php

class Transaction extends Model
{
//columns: id, amount,...

    public function documents()
    {
        return $this->belongsToMany(Document::class)
            ->using(DocumentTransaction::class)
            ->wherePivot('deleted_at', null)
            ->withPivot(['amount'])
            ->withTimestamps();
    }
}
0 likes
4 replies
abolfazlxyz's avatar

Thanks for your response. But sadly this won't solve my problem... And no, I just took advantage of the pivot, But aside from soft deleting, I still should be able to store extra attributes on the pivot table. It's actually documented here: https://laravel.com/docs/9.x/eloquent-relationships#retrieving-intermediate-table-columns

At a very basic level, I need to know how much of each transaction amount is used for each document... I think it's too simple for dealing with another model. I can only call $document->sync([$transactionId => ['amount' => 1234]], ...) and done. And yeah I may or not use soft deleting, I'm even loading my traits with help of the pivot model... It's actually working great... I only have problem with the model values (id, amount) getting override by pivot values (id, amount).

But for the sake of curiosity I actually test it without the pivot model too...

public function transactions()
    {
        return $this->belongsToMany(Transaction::class)
            ->withPivot(['amount'])
            ->withTimestamps();
    }

Now I run:

$document = Document::find(1525);
dd($document->transactions->toArray());

I got:

array:1 [▼ // Array of document's transactions
  0 => array:26 [▼ // First transaction attributes
    "id" => 1531  // ❌ wrong. It's id of the pivot row. (Must be 773, the transaction id)
    "amount" => "470000.00"  // ❌ wrong. It's amount stored in pivot. (Must be 489000 the transaction amount)
	"created_at" => '...'  // ❌ wrong. It's created_at value of pivot
	"updated_at" => '...'  // ❌ wrong. It's updated_at value of pivot
    "document_id" => 1525  // Unwanted but right (not a column in transactions table)
    "transaction_id" => 773  // Unwanted but right (not a column in transactions table)
	...

    "pivot" => array:5 [▼  // ✓ right: the pivot information is returned!
      "document_id" => 1525  // ✓ right: the id of the current document in the pivot
      "transaction_id" => 773   // ✓ right: the id of the related transaction in the pivot
      "amount" => "470000.00"   // ✓ right: the amount in the pivot
      "created_at" => ...		// ✓
      "updated_at" => ...	// ✓
    ]
  ]
]

I just noticed transaction timestamps are getting override by pivot's timestamps too! I'm sure something is wrong... If not then what's the use of chaining ->withTimestamps() to relation definition?! I even got the same behavior by this relation definnition:

public function transactions() {
        return $this->belongsToMany(Transaction::class);
}

The exact same result returned. I only lost amount and timestamps from pivot array!!!

Let's say I only save transaction_id, document_id and one extra shared_amount in the pivot table. Is it normal that when I call for transactions from a document I get shared_amount too in the transaction attributes array? So why we have ->withPivot(...) function?

Thank you :)

travis.elkins's avatar

I've just run into the same problem, but with version 10.

My pivot model isn't using soft deletes, so, as you already noted, it's not a factor.

I've just started to troubleshoot...enough to realize what is actually happening. Now I've got to figure out where the bug is.

I'll update any progress here, if I can.... 😅

KalimeroMK's avatar

Understanding the Problem Both your Document and Transaction models, as well as your DocumentTransaction pivot model, have an amount column. When you access the transactions relation on a Document instance (or documents on a Transaction), Laravel fetches the amount from the pivot table (DocumentTransaction) and includes it in the results. If a column in the pivot table has the same name as a column in the related model, the pivot table's value will override the related model's value. Solutions Rename Pivot Table Columns: This is the simplest solution. Rename the amount column in the pivot table to something like pivot_amount. This way, there's no conflict between the column names. Update your withPivot() calls accordingly. Access Pivot Data Explicitly: If renaming is not feasible, ensure that you access the pivot data explicitly when needed. For example, use $transaction->pivot->amount to get the pivot amount. However, this doesn't prevent the overriding issue but makes it clear when you're accessing pivot data. Customizing the Pivot Object: You can customize the pivot object (DocumentTransaction) to differentiate between pivot and parent model attributes. You might add custom accessors in the DocumentTransaction class that return the desired attributes, either from the pivot table or the related model. Post-Processing the Results: After fetching the results, you could iterate over them and manually adjust the attributes, but this is not an efficient solution and is more of a workaround.

Rename the column in the migration for the pivot table:

Schema::table('document_transaction', function (Blueprint $table) {
    $table->renameColumn('amount', 'pivot_amount');
});

Update your relationship methods:

// In Document.php and Transaction.php
public function transactions()
{
    return $this->belongsToMany(Transaction::class)
        ->using(DocumentTransaction::class)
        ->wherePivot('deleted_at', null)
        ->withPivot(['pivot_amount'])
        ->withTimestamps();
}

This way, you can differentiate between amount from the Document or Transaction model and pivot_amount from the DocumentTransaction pivot model.

Please or to participate in this conversation.