Domanda Laravel 5.1 ORM eloquente restituisce in modo casuale una relazione errata - * aggiornamento principale *


Ho una app Laravel che alimenta un sito di e-commerce con traffico moderato. Questo sito Web consente alle persone di effettuare ordini tramite il frontend, ma ha anche funzionalità di back-end per prendere ordini al telefono tramite un call center.

Un ordine è correlato a un cliente e un cliente può essere facoltativamente un utente - un utente che è qualcuno con un accesso al frontend. Un cliente senza account utente viene creato solo come risultato di un ordine effettuato tramite il call center.

Il problema che ho riscontrato è molto strano, e credo che potrebbe essere una specie di bug Laravel.

Succede solo occasionalmente, ma quello che succede è che quando un ordine viene preso tramite il call center per un cliente senza account utente, viene inviata una conferma d'ordine a un utente casuale - letteralmente casuale, per quanto posso dire , appena strappato dal database nonostante nessuna relazione nei dati.

Queste sono le parti rilevanti dei modelli nel progetto:

class Order extends Model
{
    public function customer()
    {
        return $this->belongsTo('App\Customer');
    }
}

class Customer extends Model
{
    public function orders()
    {
        return $this->hasMany('App\Order');
    }

    public function user()
    {
        return $this->belongsTo('App\User');
    }
}

class User extends Model
{ 
    public function customer()
    {
        return $this->hasOne('App\Customer');
    }
}

Queste sono le migrazioni del database per quanto sopra (modificato per brevità):

   Schema::create('users', function (Blueprint $table) {
        $table->increments('id');
        $table->string('first_name');
        $table->string('last_name');
        $table->string('email')->unique();
        $table->string('password', 60);
        $table->boolean('active');
        $table->rememberToken();
        $table->timestamps();
        $table->softDeletes();
    });

    Schema::create('customers', function(Blueprint $table)
    {
        $table->increments('id');
        $table->integer('user_id')->nullable->index();
        $table->string('first_name');
        $table->string('last_name');
        $table->string('telephone')->nullable();
        $table->string('mobile')->nullable();
        $table->timestamps();
        $table->softDeletes();
    });

    Schema::create('orders', function(Blueprint $table)
    {
        $table->increments('id');
        $table->integer('payment_id')->nullable()->index();
        $table->integer('customer_id')->index();
        $table->integer('staff_id')->nullable()->index();
        $table->decimal('total', 10, 2);
        $table->timestamps();
        $table->softDeletes();
    });

La logica che invia la conferma dell'ordine è all'interno di un gestore eventi che viene generato dopo che l'ordine è stato pagato.

Ecco il OrderSuccess evento (modificato per brevità):

namespace App\Events;

use App\Events\Event;
use App\Order;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;


class OrderSuccess extends Event
{
    use SerializesModels;

    public $order;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }
}

Come si può vedere, questo evento è passato a Order oggetto modello.

Ecco il gestore di eventi (modificato per brevità):

/**
 * Handle the event.
 *
 * @param  OrderSuccess  $event
 * @return void
 */
public function handle(OrderSuccess $event)
{
    // set order to paid
    $order = $event->order;
    $order->paid = date('Y-m-d H:i:s');
    $order->save();

    if(!is_null($order->customer->user)) {

        App_log::add('customer_order_success_email_sent', 'Handlers\Events\OrderSuccessProcess\handle', $order->id, print_r($order->customer, true).PHP_EOL.print_r($order->customer->user, true));

        // email the user the order confirmation
        Mail::send('emails.order_success', ['order' => $order], function($message) use ($order)
        {
            $message->to($order->customer->user->email, $order->customer->first_name.' '.$order->customer->last_name)->subject('Order #'.$order->id.' confirmation');
        });
    }

}

C'è un controllo per vedere se il $order->customer->user l'oggetto non è nullo e, se è vero, viene inviata una conferma d'ordine. Se è nullo (che frequentemente lo è), non viene inviata alcuna conferma.

Come si può vedere da quanto sopra, ho aggiunto un log per registrare gli oggetti quando viene inviata una email. Ecco un esempio di errore (ancora una volta troncato per brevità):

App\Customer Object
(
[attributes:protected] => Array
    (
        [id] => 10412
        [user_id] => 
        [first_name] => Joe
        [last_name] => Bloggs
        [telephone] => 0123456789
        [created_at] => 2015-09-14 13:09:45
        [updated_at] => 2015-10-24 05:00:01
        [deleted_at] => 
    )

[relations:protected] => Array
    (
        [user] => App\User Object
            (
                [attributes:protected] => Array
                    (
                        [id] => 1206
                        [email] => johndoe@whoknows.com
                        [password] => hashed
                        [remember_token] => 
                        [created_at] => 2015-09-19 09:47:16
                        [updated_at] => 2015-09-19 09:47:16
                        [deleted_at] => 
                    )
            )

    )

[morphClass:protected] => 
[exists] => 1
[wasRecentlyCreated] => 
[forceDeleting:protected] => 
)

App\User Object
(
[attributes:protected] => Array
    (
        [id] => 1206
        [email] => johndoe@whoknows.com
        [password] => hashed
        [remember_token] => 
        [created_at] => 2015-09-19 09:47:16
        [updated_at] => 2015-09-19 09:47:16
        [deleted_at] => 
    )

[morphClass:protected] => 
[exists] => 1
[wasRecentlyCreated] => 
[forceDeleting:protected] => 
)

Come puoi vedere, non esiste user_id per Customere tuttavia Laravel ha restituito a User oggetto.

Per di più, se licenzio manualmente lo stesso OrderSuccess evento, quanto sopra non è riproducibile - non invia l'e-mail e non carica a User oggetto.

Come ho detto prima, questo problema sta accadendo molto raramente - ci sono in media circa 40 ordini al giorno tramite il call center per i clienti senza account utente, e il problema evidenziato potrebbe verificarsi solo una o due volte alla settimana.

Non sono abbastanza familiare con Laravel per sapere quale potrebbe essere il problema qui - è una qualche forma di caching del modello, un problema con Eloquent ORM, o qualche altro gremlin nel sistema?

Qualche idea apprezzabile - potrei postare questo problema nel tracker dei problemi github di Laravel se sembra essere una qualche forma di bug.

Aggiornare In relazione ad alcune delle risposte / commenti proposti, ho cercato di rimuovere tutti i potenziali problemi di ORM Eloquent, per recuperare i dati manualmente, in questo modo:

$customer = Customer::find($order->customer_id);
$user = User::find($customer->user_id);

if(!is_null($user)) {
    // send email and log actions etc
}

Quanto sopra è ancora producendo gli stessi risultati casuali - gli utenti non collegati vengono recuperati anche quando il cliente non ha user_id (è NULL in questo caso).

Aggiornamento 2 Poiché il primo aggiornamento non ha aiutato in alcun modo, sono tornato a utilizzare l'approccio Eloequent originale. Per provare un'altra soluzione, ho tolto il mio codice evento dal gestore eventi e l'ho inserito nel mio controller: in precedenza ero licenziato dall'evento OrderSuccess utilizzando Event::fire(new OrderSuccess ($order));e invece ho commentato questa riga e ho appena inserito il codice del gestore di eventi nel metodo del controller:

$order = Order::find($order_id);

//Event::fire(new OrderSuccess ($order));

// code from the above event handler
$order->paid = date('Y-m-d H:i:s');
$order->save();

if(!is_null($order->customer->user)) {

    App_log::add('customer_order_success_email_sent', 'Handlers\Events\OrderSuccessProcess\handle', $order->id, print_r($order->customer, true).PHP_EOL.print_r($order->customer->user, true));

    // email the user the order confirmation
    Mail::send('emails.order_success', ['order' => $order], function($message) use ($order)
    {
        $message->to($order->customer->user->email, $order->customer->first_name.' '.$order->customer->last_name)->subject('Order #'.$order->id.' confirmation');
    });
}

Il cambiamento di cui sopra è stato sul sito di produzione per oltre una settimana ora - e da quel cambiamento, non c'è stata una singola istanza del problema.

L'unica conclusione possibile che riesco a raggiungere è una specie di bug nel sistema di eventi di Laravel, che in qualche modo corrompe l'oggetto passato. O qualcos'altro potrebbe essere in gioco?

Aggiornamento 3 Sembra che sia stato prematuro affermare che lo spostamento del mio codice al di fuori dell'evento ha risolto il problema - in effetti, tramite il mio logging, negli ultimi 2 giorni ho potuto vedere un po 'di conferma errata dell'ordine (5 in totale, dopo quasi 3 settimane senza problemi).

Ho notato che gli ID utente che avevano ricevuto le conferme degli ordini anomali sembravano essere in aumento (non senza lacune, ma sempre in ordine crescente).

Ho anche notato che tutti gli ordini problematici erano stati pagati tramite contanti e credito conto: la maggior parte sono solo in contanti. Ho esaminato ulteriormente questo aspetto e gli ID utente sono in realtà gli ID delle relative transazioni di credito!

Quanto sopra è la prima svolta in ghisa nel tentativo di risolvere questo problema. A un esame più attento, posso vedere che il problema è ancora casuale: ci sono alcuni ordini (almeno il 50%) che sono stati pagati tramite il credito dell'account per i clienti senza account utente, ma che non hanno causato l'invio di email errate out (nonostante l'ID della transazione del credito associato che ha una corrispondenza id utente).

Quindi, il problema è ancora casuale o apparentemente. Il mio evento di rimborso del credito è attivato in questo modo:

Event::fire(new CreditRedemption( $credit, $order ));

Quanto sopra è chiamato appena prima del mio OrderSuccess evento - come puoi vedere, entrambi gli eventi vengono superati $order oggetto modello.

Mio CreditRedemption il gestore di eventi si presenta così:

public function handle(CreditRedemption $event)
{
    // make sure redemption amount is a negative value
    if($event->credit < 0) {
        $amount = $event->credit;
    }
    else {
        $amount = ($event->credit * -1);
    }

    // create the credit transaction
    $credit_transaction = New Credit_transaction();
    $credit_transaction->transaction_type = 'Credit Redemption';
    $credit_transaction->amount = $amount; // negative value
    $credit_transaction->customer_id = $event->order->customer->id;
    $credit_transaction->order_id = $event->order->id;

    // record staff member if appropriate
    if(!is_null($event->order->staff)) {
        $credit_transaction->staff_id = $event->order->staff->id;
    }

    // save transaction
    $credit_transaction->save();

    return $credit_transaction;
}

Il $credit_transaction->save(); sta generando l'id nel mio credit_transactions tabella che è in qualche modo utilizzata da Laravel per recuperare un oggetto utente. Come si può vedere nel gestore di cui sopra, non sto aggiornando il mio $order oggetto in qualsiasi punto.

Come usa Laravel (ricorda, ancora casualmente, circa il 50% delle volte) l'id del mio nuovo creato $credit_transaciton per popolare il $order->customer->user oggetto modello?


30
2017-11-09 10:29


origine


risposte:


Non posso aiutarti a scoprire la causa di ciò che causa il tuo problema, ma posso offrire un possibile intervento in base alla logica che hai fornito nell'Aggiornamento 1 della tua domanda.

Logica originale

$customer = Customer::find($order->customer_id);
$user = User::find($customer->user_id);

if(!is_null($user)) {
    // send email and log actions etc
}

Logica riveduta

Perché un cliente user_id può essere nullo, può essere più efficace limitare i clienti restituiti a coloro che hanno un user_id. Questo può essere ottenuto usando il whereNotNull() metodo. Possiamo quindi andare avanti per verificare se un cliente è stato restituito, e in tal caso inviare l'e-mail, ecc.

$customer = Customer::whereNotNull('user_id')->find($order->customer_id); 

if (!$customer->isEmpty()) { 
    // send email and log actions etc 
}

Non dando all'applicazione la possibilità di restituire un cliente con un valore null user_id questo dovrebbe, si spera, risolvere il tuo problema, ma sfortunatamente non fa luce sul perché sta accadendo in primo luogo.


3
2017-12-02 01:58



Non dovrebbero avere le tue migrazioni

->unsigned()

per esempio:

$table->integer('user_id')->unsinged()->index();

Come menzionato nel Doc Laravel?

Laravel fornisce anche supporto per la creazione di vincoli di chiavi esterne, che vengono utilizzati per forzare l'integrità referenziale a livello di database. Ad esempio, definiamo una colonna user_id nella tabella dei post che fa riferimento alla colonna id su una tabella utenti. http://laravel.com/docs/5.1/migrations#foreign-key-constraints


1
2017-11-24 23:26



Non sono sicuro che le tue migrazioni siano definite correttamente in modo specifico rispetto ai tuoi Modelli. Se si utilizza la relazione belongsTo e hasOne, è necessario utilizzare riferimenti a chiavi esterne nelle migrazioni.

Schema::create('customers', function(Blueprint $table)
    {
        $table->increments('id');
        $table->integer('user_id')->nullable();
        $table->foreign('user_id')->references('id')->on('users');

        $table->string('first_name');
        $table->string('last_name');
        $table->string('telephone')->nullable();
        $table->string('mobile')->nullable();
        $table->timestamps();
        $table->softDeletes();
    });



Schema::create('orders', function(Blueprint $table)
{
   $table->increments('id');
   $table->integer('payment_id')->nullable()->index();

   $table->integer('customer_id')->nullable();
   $table->foreign('customer_id')->references('id')->on('customers');
   $table->integer('staff_id')->nullable()->index();
   $table->decimal('total', 10, 2);
   $table->timestamps();
   $table->softDeletes();
    });

Ora è necessario impostare questa colonna ogni volta che un utente reale esiste quando viene creato il record del cliente. Ma in realtà non devi impostare questa colonna manualmente. Puoi farlo in questo modo mentre stai usando le relazioni:

Passaggio 1: salvare prima il cliente.

$customer->save();

Passo 2: Ora imposteremo user_id sul cliente se esiste. Per fare ciò, è possibile ottenere l'oggetto utente in $ user e quindi chiamare

$customer->user->save($user);

Il codice sopra imposterà automaticamente user_id sulla tabella dei clienti

Quindi controllerò se il record dell'utente esiste in questo modo sotto:

$user_exists = $order->customer()->user();

if($user_exists)
{
    //email whatever
}

1
2017-11-10 14:25



Non è necessario avere FK. Eloquent può rogna per costruire relazioni basate sul nome delle colonne.

Cambia il nome dei campi. Il nome del campo deve corrispondere al nome della tabella con il suffisso "_id". Nella tabella clienti user_id dovrebbe essere users_id. Negli ordini customer_id dovrebbe essere customers_id.

Puoi provare a passare il nome del campo a cui vuoi unirti:

class Order extends Model
{
    public function customer()
    {
        return $this->belongsTo('App\Customer', 'foreign_key', 'id');
    }
}

Non sono un utente molto avanzato con Laravel, quindi potrebbe non funzionare. Ho avuto lo stesso problema e l'ho risolto rinominando tutti i modelli e le colonne in modo che corrispondessero ai nomi delle tabelle (con "s").


0
2017-11-10 14:45



il tuo user_id campo nella tabella dei clienti dovrebbe essere nullable.

$table->integer('user_id')->index()->nullable();

0
2017-11-26 17:05