首页 文章

Laravel 5.1 Eloquent ORM随机返回不正确的关系 - *主要更新*

提问于
浏览
30

我有一个Laravel应用程序,为电子商务网站提供适度的流量 . 该网站允许人们通过前端下订单,但它也具有通过呼叫中心通过电话接听订单的后端功能 .

订单与客户相关,客户可以选择是用户 - 用户是登录前端的用户 . 只有通过呼叫中心接收订单才能创建没有用户帐户的客户 .

我遇到的问题很奇怪,我相信可能是某种Laravel错误 .

它只是偶尔发生,但发生的事情是,当通过呼叫中心为没有用户帐户的客户下订单时,订单确认将发送给随机用户 - 字面意思是随机的,据我所知,尽管数据没有关系,但刚从数据库中拔出 .

这些是项目中模型的相关部分:

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');
    }
}

这些是上面的数据库迁移(为简洁起见编辑):

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();
    });

发送订单确认的逻辑位于在支付订单后触发的事件处理程序中 .

这是 OrderSuccess 事件(为简洁起见编辑):

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;
    }
}

可以看出,此事件传递的是 Order 模型对象 .

这是事件处理程序(为简洁起见编辑):

/**
 * 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');
        });
    }

}

检查 $order->customer->user 对象是否为空,如果为true,则发送订单确认 . 如果它为null(经常是),则不发送确认 .

从上面可以看出,我添加了一个日志来记录发送电子邮件时的对象 . 这是一个错误的例子(为简洁而再次截断):

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] => 
)

如您所见, Customer 没有user_id,但Laravel已返回 User 对象 .

更重要的是,如果我手动触发完全相同的 OrderSuccess 事件,则上述内容不可重现 - 它不会发送电子邮件,也不会加载 User 对象 .

正如我之前所说,这个问题很少发生 - 通过呼叫中心平均每天约有40个订单,没有用户帐户的客户,突出显示的问题可能每周只发生一次或两次 .

我对Laravel不太熟悉,不知道这里可能存在什么问题 - 它是某种形式的模型缓存,Eloquent ORM的问题,还是系统中的其他一些问题?

任何想法赞赏 - 我可能会在Laravel github问题跟踪器中发布此问题,如果它似乎是某种形式的错误 .

Update 关于提出的一些答案/评论,我试图删除任何潜在的Eloquent ORM问题,手动检索数据,如下所示:

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

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

以上仍然产生相同的随机结果 - 即使客户没有 user_id (在这种情况下它是NULL),也会检索不相关的用户 .

Update 2 由于第一次更新没有任何帮助,我恢复使用原始的Eloequent方法 . 为了尝试另一个解决方案,我从事件处理程序中取出了我的事件代码,并将其放在我的控制器中 - 我之前使用 Event::fire(new OrderSuccess ($order)); 通过OrderSuccess事件触发,而我将这一行注释掉,只是将事件处理程序代码放在控制器中方法:

$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');
    });
}

上述更改已在 生产环境 站点上进行了一周以上 - 自此更改以来,没有一个问题的实例 .

我能达到的唯一可能结论是Laravel事件系统中的某种错误,不知何故破坏了传递的对象 . 或者其他什么可以发挥作用?

Update 3 似乎我现在说过在事件之外移动我的代码解决问题还为时过早 - 实际上,通过我的日志记录,在过去的两天里我可以看到更多不正确的订单确认被发送出去(总共5个,几乎在3周没有问题) .

我注意到收到恶意订单确认的用户ID似乎正在递增(不是没有间隙,但仍然按升序排列) .

我还注意到每个问题订单都是通过现金和账户信贷支付的 - 大多数只是现金 . 我进一步研究了这个,用户ID实际上是相关信用交易的ID!

以上是试图解决这一问题的第一次铸铁突破 . 仔细观察后,我可以看到问题仍然是随机的 - 有很多(至少50%)订单是通过帐户信用支付给没有用户帐户的客户,但是没有导致发送不正确的电子邮件out(尽管相关的信用交易ID具有用户ID匹配) .

所以,问题仍然是随机的,或者看似如此 . 我的信用兑换事件是这样触发的:

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

在我的 OrderSuccess 事件之前调用上面的内容 - 正如您所看到的,两个事件都传递给 $order 模型对象 .

我的 CreditRedemption 事件处理程序如下所示:

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(); 正在我的 credit_transactions 表中生成id某种程度上被Laravel用来检索用户对象 . 从上面的处理程序中可以看出,我不会在任何时候更新我的 $order 对象 .

Laravel如何使用(记住,仍然是随机的,一些<50%的时间)我新创建的 $credit_transaciton 的id来填充 $order->customer->user 模型对象?

5 回答

  • 3

    您的迁移不应该

    ->unsigned()
    

    例如:

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

    正如Laravel Doc中提到的那样?

    Laravel还支持创建外键约束,这些约束用于强制数据库级别的引用完整性 . 例如,让我们在posts表上定义一个user_id列,该列引用users表上的id列 . http://laravel.com/docs/5.1/migrations#foreign-key-constraints

  • 1

    我无法帮助您了解导致问题的根本原因,但我可以根据您在问题的更新1中提供的逻辑提供可能的解决方案 .

    原始逻辑

    $customer = Customer::find($order->customer_id);
    $user = User::find($customer->user_id);
    
    if(!is_null($user)) {
        // send email and log actions etc
    }
    

    修订逻辑

    由于客户 user_id 可以为null,因此将返回的客户限制为具有 user_id 的客户可能更有效 . 这可以通过使用 whereNotNull() 方法来实现 . 然后我们可以继续检查客户是否被退回,如果是,则发送电子邮件等 .

    $customer = Customer::whereNotNull('user_id')->find($order->customer_id); 
    
    if (!$customer->isEmpty()) { 
        // send email and log actions etc 
    }
    

    通过不给应用程序一个机会返回一个null user_id 的客户,这应该有希望解决你的问题,但遗憾的是它并没有说明为什么它首先发生的原因 .

  • 0

    与模型相比,我不确定您的迁移是否特别正确定义 . 如果使用belongsTo和hasOne关系,则应在迁移中使用外键引用 .

    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();
        });
    

    现在,只要在创建客户记录时存在实际用户,您就需要设置此列 . 但实际上您不必手动设置此列 . 您可以在使用关系时执行以下操作:

    第1步:先保存客户 .

    $customer->save();
    

    第2步:现在我们将在客户端设置user_id(如果存在) . 为此,您可以在$ user中获取用户对象,然后只需调用

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

    上面的代码将自动在customers表上设置user_id

    然后我将检查用户记录是否以下面的方式存在:

    $user_exists = $order->customer()->user();
    
    if($user_exists)
    {
        //email whatever
    }
    
  • 1

    没有必要有FK . Eloquent可以根据列的名称来构建关系 .

    更改字段的名称 . 该字段的名称应与具有后缀“_id”的表名匹配 . 在customers表中,user_id应为users_id . 在订单中,customer_id应为customers_id .

    您可以尝试传递要加入的字段的名称:

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

    我不是Laravel的高级用户,所以这可能不起作用 . 我遇到了同样的问题,我通过重命名所有模型和列来匹配表名(用“s”)来解决它 .

  • 0

    customers表中的 user_id 字段应该可以为空 .

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

相关问题