简体   繁体   English

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

[英]Laravel 5.1 Eloquent ORM randomly returning incorrect relationship - *major update*

I have a Laravel app that powers an ecommerce website with moderate traffic. 我有一个Laravel应用程序,为电子商务网站提供适度的流量。 This website allows people to place orders via the frontend, but it also has backend functionality for taking orders over the phone via a call centre. 该网站允许人们通过前端下订单,但它也具有通过呼叫中心通过电话接收订单的后端功能。

An order is related to a customer, and a customer can optionally be a user - a user being someone with a login to the frontend. 订单与客户相关,客户可以选择是用户 - 用户是登录前端的用户。 A customer with no user account is only ever created as a result of an order being taken via the call centre. 只有通过呼叫中心下达订单才能创建没有用户帐户的客户。

The issue I have encountered is very odd, and I believe might be some kind of Laravel bug. 我遇到的问题非常奇怪,我相信可能是某种Laravel错误。

It only occurs very occasionally, but what is happening is that when an order is taken via the call centre for a customer with no user account, an order confirmation is being sent out to a random user - literally random, as far as I can tell, just plucked out of the database despite no relation in the data. 它只是偶尔发生,但发生的事情是当通过呼叫中心为没有用户帐户的客户下订单时,订单确认正在发送给随机用户 - 字面意思是随机的,据我所知,尽管数据没有关系,但刚从数据库中拔出。

These are the relevant parts of the models in the project: 这些是项目中模型的相关部分:

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

These are the database migrations for the above (edited for brevity): 这些是上面的数据库迁移(为简洁起见编辑):

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

The logic that sends the order confirmation is within an event handler that gets fired after the order has been paid. 发送订单确认的逻辑位于支付订单后触发的事件处理程序中。

Here is the OrderSuccess event (edited for brevity): 这是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;
    }
}

As can be seen, this event is passed an Order model object. 可以看出,此事件传递了Order模型对象。

Here is the event handler (edited for brevity): 这是事件处理程序(为简洁起见编辑):

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

}

There is a check to see if the $order->customer->user object is not null and, if true, an order confirmation is sent. 检查$order->customer->user对象是否为null,如果为true,则发送订单确认。 If it is null (which it frequently is), then no confirmation is sent. 如果它为null(经常是),则不发送确认。

As can be seen from the above, I added a log to record the objects when an email is sent. 从上面可以看出,我添加了一个日志来记录发送电子邮件时的对象。 Here is an example of an erroneous one (again, truncated for brevity): 这是一个错误的例子(为简洁起见,再次被截断):

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

As you can see, there is no user_id for Customer , and yet Laravel has returned a User object. 如您所见, Customer没有user_id,但Laravel已返回User对象。

What is more, if I manually fire the exact same OrderSuccess event, the above is not reproducible - it does not send the email, and it does not load a User object. 更重要的是,如果我手动触发完全相同的OrderSuccess事件,则上述内容不可重现 - 它不会发送电子邮件,也不会加载User对象。

As I said before, this issue is happening very infrequently - there are on average about 40 orders a day via the call centre for customers with no user account, and the highlighted issue might only occur once or twice a week. 正如我之前所说,这个问题很少发生 - 通过呼叫中心平均每天约有40个订单,没有用户帐户的客户,突出显示的问题可能每周只发生一次或两次。

I'm not familiar enough with Laravel to know what might be the issue here - is it some form of model caching, a problem with Eloquent ORM, or some other gremlin in the system? 我对Laravel不太熟悉,不知道这里可能存在什么问题 - 它是某种形式的模型缓存,Eloquent ORM的问题,还是系统中的其他一些问题?

Any ideas appreciated - I may post this problem in the Laravel github issue tracker if it appears to be some form of bug. 任何想法赞赏 - 我可能会在Laravel github问题跟踪器中发布此问题,如果它似乎是某种形式的错误。

Update Relating to some of the answers / comments put forward, I have tried to remove any potential Eloquent ORM issues, to retrieve the data manually, like so: 更新关于提出的一些答案/评论,我试图删除任何潜在的Eloquent ORM问题,手动检索数据,如下所示:

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

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

The above is still producing the same random results - unrelated users are being retrieved even when the customer has no user_id (it's NULL in this instance). 以上仍然产生相同的随机结果 - 即使客户没有user_id (在这种情况下它是NULL),也会检索不相关的用户。

Update 2 As the first update did not help in any way, I reverted to using the original Eloequent approach. 更新2由于第一次更新没有任何帮助,我恢复使用原始的Eloequent方法。 To try another solution, I took my event code out of the event handler, and placed it in my controller - I was previously firing by OrderSuccess event using Event::fire(new OrderSuccess ($order)); 为了尝试另一个解决方案,我将事件代码从事件处理程序中取出,并将其放在我的控制器中 - 我以前使用Event::fire(new OrderSuccess ($order));通过OrderSuccess事件触发Event::fire(new OrderSuccess ($order)); , and instead I commented this line out and just placed the event handler code in the controller method: ,而是我评论了这一行,并将事件处理程序代码放在控制器方法中:

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

The above change has been on the production site for over a week now - and since that change, there has not been a single instance of the issue. 上述更改已在生产站点上进行了一周以上 - 自此更改以来,没有一个问题的实例。

The only possible conclusion I can reach is some kind of bug in the Laravel event system, somehow corrupting the passed object. 我能达到的唯一可能的结论是Laravel事件系统中的某种错误,不知何故破坏了传递的对象。 Or could something else be at play? 或者其他什么东西可以发挥作用?

Update 3 It seems I was premature to state that moving my code outside the event fixed the issue - in fact, via my logging, in the last 2 days I can see some more incorrect order confirmation were sent out (a total of 5, after almost 3 weeks of no issues). 更新3似乎我说过在事件之外移动我的代码修复问题还为时过早 - 实际上,通过我的日志记录,在最近2天我可以看到更多不正确的订单确认被发送出去(总共5个,之后差不多3个星期没有问题)。

I noticed that the user ids that had received the rogue order confirmations appeared to be incrementing (not without gaps, but still in ascending order). 我注意到收到恶意订单确认的用户ID似乎正在递增(不是没有间隙,但仍然按升序排列)。

I also noticed that each of the problem orders had been paid for via cash and account credit - most are cash only. 我还注意到每个问题订单都是通过现金和账户信贷支付的 - 大多数只是现金。 I looked further into this, and the user ids are actually the ids of the related credit transactions! 我进一步研究了这个,用户ID实际上是相关信用交易的ID!

The above is the first cast iron breakthrough in trying to resolve this issue. 以上是试图解决这一问题的第一次铸铁突破。 On closer inspection, I can see that the issue is still random - there are quite a few (at least 50%) orders that have been paid via account credit for customer's with no user account, but that have not caused incorrect emails to be sent out (despite the associated credit transaction id having a user id match). 经过仔细检查,我可以看到问题仍然是随机的 - 有很多(至少50%)订单已通过帐户信用支付给没有用户帐户的客户,但没有导致发送不正确的电子邮件out(尽管相关的信用交易ID具有用户id匹配)。

So, the problem is still random, or seemingly so. 所以,问题仍然是随机的,或者看似如此。 My credit redemption event is triggered like so: 我的信用兑换事件是这样触发的:

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

The above is called just prior to my OrderSuccess event - as you can see, both events are passed the $order model object. 以上是在我的OrderSuccess事件之前调用的 - 正如您所看到的,两个事件都传递了$order模型对象。

My CreditRedemption event handler looks like this: 我的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;
}

The $credit_transaction->save(); $credit_transaction->save(); is generating the id in my credit_transactions table that is somehow being used by Laravel to retrieve a user object. 正在我的credit_transactions表中生成一个id, credit_transactions表以某种方式被Laravel用于检索用户对象。 As can be seen in the above handler, I'm not updating my $order object at any point. 从上面的处理程序中可以看出,我不会在任何时候更新我的$order对象。

How is Laravel using (remember, still randomly, some < 50% of the time) the id of my newly created $credit_transaciton to populate the $order->customer->user model object? Laravel如何使用(记住,仍然是随机的,一些<50%的时间)我新创建的$credit_transaciton的id来填充$order->customer->user模型对象?

I can't help you get to the root of whats causing your issue, but I can offer a possible work around based on the logic you provided in Update 1 of your question. 我无法帮助您了解导致问题的根本原因,但我可以根据您在问题的更新1中提供的逻辑提供可能的解决方法。

Original Logic 原始逻辑

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

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

Revised Logic 修订逻辑

Because a customers user_id can be null, it may be more effective to limit the returned customers to those who have a user_id . 由于客户user_id可以为null,因此将返回的客户限制为具有user_id客户可能更有效。 This can be achieved by using the whereNotNull() method. 这可以通过使用whereNotNull()方法来实现。 We can then go on to check if a customer was returned, and if so send the email, etc. 然后我们可以继续检查客户是否被退回,如果是,则发送电子邮件等。

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

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

By not giving the application a chance to return a customer with a null user_id this should hopefully solve your issue, but unfortunately it doesn't shine any light on why it is happening in the first place. 通过不给应用程序提供返回具有null user_id的客户的机会,这应该有望解决您的问题,但不幸的是,它并没有说明为什么它首先发生的原因。

I am not sure if your migrations are defined correctly specially when compared to your Models. 与模型相比,我不确定您的迁移是否特别正确定义。 If you are using belongsTo and hasOne relationship, you should use foreign key references in migrations. 如果使用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();
    });

Now you will need to set this column whenever an actual user exists when the customer record is being created. 现在,只要在创建客户记录时存在实际用户,您就需要设置此列。 But you don't actually have to set this column manually. 但实际上您不必手动设置此列。 You can do this below as you are using relationships: 您可以在使用关系时执行以下操作:

Step 1: Save the customer first. 第1步:先保存客户。

$customer->save();

Step 2: Now we will set the user_id on the customer if exists. 第2步:现在我们将在客户端设置user_id(如果存在)。 To do this, you can get the user object in $user and then just call 为此,您可以在$ user中获取用户对象,然后只需调用

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

The code above will automatically set the user_id on customers table 上面的代码将自动在customers表上设置user_id

Then I will check if user record exists in this way below: 然后我将检查用户记录是否以这种方式存在:

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

if($user_exists)
{
    //email whatever
}

Shouldn't your migrations have 您的迁移不应该

->unsigned()

for example: 例如:

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

As mentioned in the Laravel Doc? 正如Laravel Doc中提到的那样?

Laravel also provides support for creating foreign key constraints, which are used to force referential integrity at the database level. Laravel还支持创建外键约束,这些约束用于强制数据库级别的引用完整性。 For example, let's define a user_id column on the posts table that references the id column on a users table. 例如,让我们在posts表上定义一个user_id列,该列引用users表上的id列。 http://laravel.com/docs/5.1/migrations#foreign-key-constraints http://laravel.com/docs/5.1/migrations#foreign-key-constraints

It is not necessary to have FK. 没有必要有FK。 Eloquent can mange to build relations based on the name of the columns. Eloquent可以根据列的名称来构建关系。

Change the name of the fields. 更改字段的名称。 The name of the field should match table name with suffix "_id". 该字段的名称应与具有后缀“_id”的表名匹配。 In the customers table user_id should be users_id. 在customers表中,user_id应为users_id。 In orders customer_id should be customers_id. 在订单中,customer_id应为customers_id。

You can try to pass the name of the field you want to join with: 您可以尝试传递要加入的字段的名称:

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

I am not very advanced user with Laravel so this might not work. 我不是Laravel的高级用户,所以这可能不起作用。 I had the same issue and I solved it by renaming all models and columns to match table names (with "s"). 我遇到了同样的问题,我通过重命名所有模型和列来匹配表名(用“s”)来解决它。

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM