php - updateorcreate - Laravel 5.1 Eloquent ORM devuelve al azar una relación incorrecta-* actualización importante*
updateorcreate laravel (5)
Tengo una aplicación Laravel que impulsa un sitio web de comercio electrónico con tráfico moderado. Este sitio web permite a las personas hacer pedidos a través de la interfaz, pero también tiene la funcionalidad de backend para tomar pedidos por teléfono a través de un centro de llamadas.
Una orden se relaciona con un cliente, y un cliente puede ser opcionalmente un usuario, un usuario que es alguien con un inicio de sesión en la interfaz. Un cliente sin cuenta de usuario solo se crea como resultado de un pedido que se realiza a través del centro de llamadas.
El problema que he encontrado es muy extraño, y creo que podría ser algún tipo de error de Laravel.
Solo ocurre muy ocasionalmente, pero lo que sucede es que cuando se realiza un pedido a través del centro de llamadas para un cliente sin cuenta de usuario, se envía una confirmación de pedido a un usuario aleatorio, literalmente al azar, hasta donde sé , simplemente arrancado de la base de datos a pesar de no tener relación con los datos.
Estas son las partes relevantes de los modelos en el proyecto:
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'');
}
}
Estas son las migraciones de la base de datos para lo anterior (editadas por brevedad):
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 lógica que envía la confirmación del pedido se encuentra dentro de un controlador de eventos que se activa después de que se pagó el pedido.
Aquí está el evento OrderSuccess
(editado por brevedad):
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;
}
}
Como se puede ver, a este evento se le pasa un objeto de modelo de Order
.
Aquí está el controlador de eventos (editado por brevedad):
/**
* 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'');
});
}
}
Hay un cheque para ver si el objeto $order->customer->user
no es nulo y, si es verdadero, se envía una confirmación de pedido. Si es nulo (que es con frecuencia), entonces no se envía confirmación.
Como se puede ver en lo anterior, agregué un registro para registrar los objetos cuando se envía un correo electrónico. Aquí hay un ejemplo de uno erróneo (una vez más, truncado por brevedad):
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] => [email protected]
[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] => [email protected]
[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] =>
)
Como puede ver, no hay user_id para el Customer
, pero Laravel ha devuelto un objeto User
.
Además, si OrderSuccess
manualmente el mismo evento OrderSuccess
, lo anterior no es reproducible: no envía el correo electrónico y no carga un objeto User
.
Como dije antes, este problema ocurre con poca frecuencia: hay un promedio de 40 pedidos por día a través del centro de llamadas para clientes sin cuenta de usuario, y el problema resaltado solo puede ocurrir una o dos veces por semana.
No estoy lo suficientemente familiarizado con Laravel como para saber cuál podría ser el problema aquí: ¿es alguna forma de almacenamiento en caché de modelos, un problema con Eloquent ORM o algún otro gremlin en el sistema?
Cualquier idea apreciada: puedo publicar este problema en el rastreador de problemas de Laravel github si parece ser algún tipo de error.
Actualización En relación con algunas de las respuestas / comentarios presentados, intenté eliminar cualquier posible problema Eloquent de ORM, para recuperar los datos manualmente, así:
$customer = Customer::find($order->customer_id);
$user = User::find($customer->user_id);
if(!is_null($user)) {
// send email and log actions etc
}
Lo anterior sigue produciendo los mismos resultados aleatorios: se están recuperando usuarios no relacionados incluso cuando el cliente no tiene user_id
(es NULL en este caso).
Actualización 2 Como la primera actualización no ayudó de ninguna manera, volví a usar el enfoque Eloequent original. Para probar otra solución, saqué mi código de evento del controlador de eventos y lo coloqué en mi controlador: antes estaba activando el evento OrderSuccess usando Event::fire(new OrderSuccess ($order));
, y en su lugar, comenté esta línea y simplemente coloqué el código del controlador de eventos en el método del controlador:
$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'');
});
}
El cambio anterior ha estado en el sitio de producción durante más de una semana, y desde ese cambio, no ha habido una sola instancia del problema.
La única conclusión posible que puedo alcanzar es algún tipo de error en el sistema de eventos de Laravel, que de alguna manera corrompe el objeto pasado. ¿O podría otra cosa estar en juego?
Actualización 3 Parece que era prematuro afirmar que mover mi código fuera del evento solucionó el problema; de hecho, a través de mi registro, en los últimos 2 días pude ver que se enviaron más notificaciones de pedido incorrectas (un total de 5, después de casi 3 semanas sin problemas).
Noté que los identificadores de usuario que habían recibido las confirmaciones de orden deshonesto parecían estar incrementándose (no sin vacíos, sino en orden ascendente).
También noté que cada una de las órdenes problemáticas se había pagado en efectivo y con crédito de cuenta, la mayoría solo en efectivo. ¡Indagué más en esto, y los ID de usuario son en realidad los identificadores de las transacciones de crédito relacionadas!
Lo anterior es el primer avance de hierro fundido en tratar de resolver este problema. En una inspección más cercana, puedo ver que el problema sigue siendo aleatorio: hay bastantes (al menos 50%) pedidos que se han pagado a través de crédito de cuenta para clientes sin cuenta de usuario, pero que no han enviado correos electrónicos incorrectos. out (a pesar de la identificación de transacción de crédito asociada que tiene una coincidencia de id de usuario).
Entonces, el problema sigue siendo aleatorio, o aparentemente así. Mi evento de canje de crédito se desencadena de la siguiente manera:
Event::fire(new CreditRedemption( $credit, $order ));
Lo anterior se llama justo antes de mi evento OrderSuccess
: como puede ver, ambos eventos pasan el $order
modelo $order
.
Mi CreditRedemption
eventos CreditRedemption
se ve así:
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;
}
$credit_transaction->save();
está generando la identificación en mi tabla credit_transactions
que de alguna manera está siendo utilizada por Laravel para recuperar un objeto de usuario. Como se puede ver en el controlador anterior, no estoy actualizando mi objeto $order
en ningún momento.
¿Cómo está usando Laravel (recuerde, aún aleatoriamente, algo de <50% del tiempo) el id de mi $credit_transaciton
recién creado para rellenar el objeto de modelo $credit_transaciton
$order->customer->user
?
¿No deberían sus migraciones tener
->unsigned()
por ejemplo:
$table->integer(''user_id'')->unsinged()->index();
Como se menciona en el Doc Laravel?
Laravel también proporciona soporte para crear restricciones de clave externa, que se utilizan para forzar la integridad referencial a nivel de base de datos. Por ejemplo, definamos una columna user_id en la tabla de publicaciones que hace referencia a la columna de id en una tabla de usuarios. http://laravel.com/docs/5.1/migrations#foreign-key-constraints
No es necesario tener FK. Eloquent puede crear relaciones basadas en el nombre de las columnas.
Cambia el nombre de los campos. El nombre del campo debe coincidir con el nombre de la tabla con el sufijo "_id". En la tabla de clientes user_id debe ser users_id. En los pedidos, customer_id debe ser customers_id.
Puede intentar pasar el nombre del campo al que desea unirse:
class Order extends Model
{
public function customer()
{
return $this->belongsTo(''App/Customer'', ''foreign_key'', ''id'');
}
}
No soy un usuario muy avanzado con Laravel, así que puede que esto no funcione. Tuve el mismo problema y lo resolví cambiando el nombre de todos los modelos y columnas para que coincidan con los nombres de las tablas (con "s").
No estoy seguro si sus migraciones están definidas correctamente, especialmente en comparación con sus Modelos. Si está utilizando la relación belongsTo y hasOne, debe usar referencias de clave externa en migraciones.
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();
});
Ahora deberá configurar esta columna siempre que exista un usuario real cuando se esté creando el registro del cliente. Pero en realidad no tiene que configurar esta columna manualmente. Puede hacer esto a continuación mientras usa relaciones:
Paso 1: guarde al cliente primero.
$customer->save();
Paso 2: Ahora estableceremos el user_id en el cliente si existe. Para hacer esto, puede obtener el objeto de usuario en $ usuario y luego simplemente llamar
$customer->user->save($user);
El código anterior configurará automáticamente el ID_usuario en la tabla de clientes
Luego verificaré si el registro del usuario existe de la siguiente manera:
$user_exists = $order->customer()->user();
if($user_exists)
{
//email whatever
}
No puedo ayudarlo a llegar a la raíz de cuál es la causa de su problema, pero puedo ofrecerle un posible trabajo basado en la lógica que proporcionó en la Actualización 1 de su pregunta.
Lógica original
$customer = Customer::find($order->customer_id);
$user = User::find($customer->user_id);
if(!is_null($user)) {
// send email and log actions etc
}
Lógica revisada
Como un user_id
un cliente puede ser nulo, puede ser más efectivo limitar los clientes devueltos a aquellos que tienen un user_id
. Esto se puede lograr utilizando el método whereNotNull()
. Luego podemos verificar si un cliente fue devuelto y, de ser así, enviar el correo electrónico, etc.
$customer = Customer::whereNotNull(''user_id'')->find($order->customer_id);
if (!$customer->isEmpty()) {
// send email and log actions etc
}
Al no darle a la aplicación la oportunidad de devolver un cliente con un nulo user_id
esto debería resolver su problema, pero lamentablemente no aclara por qué está sucediendo en primer lugar.
su campo user_id
en la tabla de clientes debe ser anulable.
$table->integer(''user_id'')->index()->nullable();