Laravel 访问器,你真的用好了吗?(大坑实践)

原文地址

Laravel 访问器,你真的用好了吗?(大坑实践)

啥?重新学习 Laravel Eloquent:访问器?为什么要重新学习这玩意?

最近有反应说客户列表页面反应较慢,我测试了一下,使用体验确实很差,特别慢。后来查日志才知道,是在循环体中使用了一个定义好的一个访问器,这个访问器访问了数据库,但是相关数据库是做了关联预查询的,所以这种情况的发生是异常的。

那么问题来了,为什么呢?

带着这样的疑问,我决定忘记所有,从一个小白的态度,重新学习一下 Laravel Eloquent:访问器。

开始学习

开始实验之前,对这一块的使用方法还不了解的建议先看文档——Eloquent: 访问器了解大概

这里先描述本次实验的大致情况

  • version: Laravel5.5
  • Model:
    • customer -> customer_tags 一对多
    • customer_tags -> tags 一对一
  • 需求:
    • 将每个客户所有的 tag 转成字符串用/隔开返回

编写代码

我们需要一个在控制器中定义一个 function 处理请求返回数据。定义一个访问器来实现需求返回给前端

准备工作

脱敏,去除无关字段。

辅助方法

  • responseSuccess() 返回给前端前对数据进行格式处理

  • iteratorGet() 从一个数组或者对象中获取一个元素,如果没有就返回 null

// helpers.php

//对返回给前端前对数据进行格式处理
function responseSuccess($data = [], $message = '操作成功')
{
    $res = [
        'msg'  => $message,
        'code' => 200,
        'data' => $data
    ];
    //分页特殊处理
    if ($data instanceof Paginator) {
        $data = $data->toArray();
        $page = [
            'current_page' => $data['current_page'],
            'last_page'    => $data['last_page'],
            'per_page'     => $data['per_page'],
            'total'        => $data['total']
        ];

        $res['data']  = $data['data'];
        $res['pages'] = $page;
    }
    return response()->json($res)->setStatusCode(200);
}
// 从一个数组或者对象中获取一个元素,如果没有就返回null
function iteratorGet($iterator, $key, $default = null)
{
    //代码省略,见谅
    ...
}

定义访问器

定义访问器,将当前客户所有的 tag 转成字符串用/隔开返回

// App/Models/Customer
public function getTestTagAttribute()
{
    $customerTags = iteratorGet($this, 'customerTags', []);
    $tags         = [];
    foreach ($customerTags as $customerTag) {
        $tags[] = iteratorGet($customerTag->tag, 'name');
    }
    return implode('/', $tags);
}

控制器方法

处理请求返回数据

// CustomerController
public function testCustomer()
    try {
        $beginTime = microtime(true);
        /** @var Collection $customers */
        $customers = Customer::with('customerTags.tag')->select(['id'])->limit(15)->get();
        $endTime = microtime(true);
        \Log::info($endTime - $beginTime);
        return responseSuccess($customers);
    } catch (\Exception $e) {
        errorLog($e);
        return responseFailed($e->getMessage());
    }
}

第一次请求

返回的字段中并没有我们想要的数据

结果

{
    "msg": "操作成功",
    "code": 200,
    "data": [
        {
            "id": 92424,
            "customer_tags": [
                {
                    "id": 1586,
                    "customer_id": 92424,
                    "tag_id": 1,
                    "tag": {
                        "id": 1,
                        "name": "年龄太小",
                    }
                },
                {
                    "id": 1588,
                    "customer_id": 92424,
                    "tag_id": 2,
                    "tag": {
                        "id": 2,
                        "name": "零基础",
                    }
                },
                {
                    "id": 1587,
                    "customer_id": 92424,
                    "tag_id": 10,
                    "tag": {
                        "id": 10,
                        "name": "年龄过大",
                    }
                }
            ]
        },
        {
            "id": 16,
            "customer_tags": []
        },
        ...
    ]
}

分析

为什么会没有呢?难道定义的访问器并不能访问数据?还是说没有被调用呢?
我们在 tinker 中查询一个 Customer,看一下 Customer 打印的结果

>>> $c = Customer::find(92424);
=> App\Models\Customer {#3493
     id: 92424,
     category: 0,
     name: "sadas",
   }
>>> $c->test_tag
=> "年龄太小/零基础/年龄过大"
>>> $c
=> App\Models\Customer {#3493
     id: 92424,
     category: 0,
     name: "sadas",
     customerTags: Illuminate\Database\Eloquent\Collection {#3487
       all: [
         App\Models\CustomerTag {#3498
           id: 1586,
           customer_id: 92424,
           tag_id: 1,
           tag: App\Models\Tag {#3504
             id: 1,
             name: "年龄太小",
           },
         },
         App\Models\CustomerTag {#3499
           id: 1588,
           customer_id: 92424,
           tag_id: 2,
           tag: App\Models\Tag {#211
             id: 2,
             name: "零基础",
           },
         },
         App\Models\CustomerTag {#3500
           id: 1587,
           customer_id: 92424,
           tag_id: 10,
           tag: App\Models\Tag {#3475
             id: 10,
             name: "年龄过大",
           },
         },
       ],
     },
   }

Customer 中并没有与 test_tags 属性,也没有相关信息。为什么我们执行 $c->test_tag 是可以执行我们定义的访问器呢?

不要着急,慢慢回顾一下 phpoop ,我们都知道 php 有很多的魔术方法。

魔术方法

__construct()__destruct()__call()__callStatic()__get()__set()__isset()__unset()__sleep()__wakeup()__toString()__invoke()__set_state()__clone()__debugInfo() 等方法在 PHP 中被称为魔术方法(Magic methods)。在命名自己的类方法时不能使用这些方法名,除非是想使用其魔术功能。

Caution PHP 将所有以 (两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 为前缀。

读取不可访问属性的值时,__get() 会被调用。

所以这里我们查看 laravel 的源码,看一下 Cusomer 所继承的 Model 对象中,对 __get() 的定义

namespace Illuminate\Database\Eloquent;

abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{
    use Concerns\HasAttributes,
        Concerns\HasEvents,
        Concerns\HasGlobalScopes,
        Concerns\HasRelationships,
        Concerns\HasTimestamps,
        Concerns\HidesAttributes,
        Concerns\GuardsAttributes;

    /**
     * Dynamically retrieve attributes on the model.
     *
     * @param  string  $key
     * @return mixed
     */
    public function __get($key)
    {
        return $this->getAttribute($key);
    }

    /**
     * Dynamically set attributes on the model.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return void
     */
    public function __set($key, $value)
    {
        $this->setAttribute($key, $value);
    }
}

getAttribute() 不在 Model 对象中定义,在\Illuminate\Database\Eloquent\Concerns\HasAttributes 中定义,我们看一下。

Str::studly() 是将字符串转化成大写字母开头的驼峰风格字符串

namespace Illuminate\Database\Eloquent\Concerns;

trait HasAttributes
{
     /**
     * The model's attributes.
     * 模型的属性
     *
     * @var array
     */
    protected $attributes = [];

    /**
     * Get an attribute from the model.
     *
     * @param  string  $key
     * @return mixed
     */
    public function getAttribute($key)
    {
        if (! $key) {
            return;
        }

        // If the attribute exists in the attribute array or has a "get" mutator we will
        // get the attribute's value. Otherwise, we will proceed as if the developers
        // are asking for a relationship's value. This covers both types of values.
        // 检测key是模型的属性之一或者key有对应定义的访问器,满足条件获取key对应的值
        if (array_key_exists($key, $this->attributes) ||
            $this->hasGetMutator($key)) {
            return $this->getAttributeValue($key);
        }

        // Here we will determine if the model base class itself contains this given key
        // since we don't want to treat any of those methods as relationships because
        // they are all intended as helper methods and none of these are relations.
        if (method_exists(self::class, $key)) {
            return;
        }
        //获取[关联关系relation]的值
        return $this->getRelationValue($key);
    }

    /**
     * Determine if a get mutator exists for an attribute.
     *  检查一个key是否存在对应定义的访问器
     * @param  string  $key
     * @return bool
     */
    public function hasGetMutator($key)
    {
        return method_exists($this, 'get'.Str::studly($key).'Attribute');
    }

    /**
     * Get a plain attribute (not a relationship).
     * 获取key对应的值
     * @param  string  $key
     * @return mixed
     */
    public function getAttributeValue($key)
    {
        //从已有元素中获取一个key对应的值
        $value = $this->getAttributeFromArray($key);

        // If the attribute has a get mutator, we will call that then return what
        // it returns as the value, which is useful for transforming values on
        // retrieval from the model to a form that is more useful for usage.
        //检查一个key是否存在对应定义的访问器,满足条件就返回对应访问器方法返回的值
        // (注意这里会传一个参数给对应的方法,参数的值为从已有元素中获取一个key对应的值)
        if ($this->hasGetMutator($key)) {
            return $this->mutateAttribute($key, $value);
        }

        // If the attribute exists within the cast array, we will convert it to
        // an appropriate native PHP type dependant upon the associated value
        // given with the key in the pair. Dayle made this comment line up.
        if ($this->hasCast($key)) {
            return $this->castAttribute($key, $value);
        }

        // If the attribute is listed as a date, we will convert it to a DateTime
        // instance on retrieval, which makes it quite convenient to work with
        // date fields without having to create a mutator for each property.
        if (in_array($key, $this->getDates()) &&
            ! is_null($value)) {
            return $this->asDateTime($value);
        }

        return $value;
    }

    /**
     * Get an attribute from the $attributes array.
     * 从已有元素中获取一个key对应的值
     * @param  string  $key
     * @return mixed
     */
    protected function getAttributeFromArray($key)
    {
        if (isset($this->attributes[$key])) {
            return $this->attributes[$key];
        }
    }

    /**
     * Get the value of an attribute using its mutator.
     * 返回对应访问器方法返回的值(注意这里会传一个参数给对应的方法)
     * @param  string  $key
     * @param  mixed  $value
     * @return mixed
     */
    protected function mutateAttribute($key, $value)
    {
        return $this->{'get'.Str::studly($key).'Attribute'}($value);
    }

    /**
     * Set a given attribute on the model.
     * 给对象设置一个属性
     * @param  string  $key
     * @param  mixed  $value
     * @return $this
     */
    public function setAttribute($key, $value)
    {
        // First we will check for the presence of a mutator for the set operation
        // which simply lets the developers tweak the attribute as it is set on
        // the model, such as "json_encoding" an listing of data for storage.
        // 先检查有没有定义修改器
        if ($this->hasSetMutator($key)) {
            $method = 'set'.Str::studly($key).'Attribute';

            return $this->{$method}($value);
        }

        // If an attribute is listed as a "date", we'll convert it from a DateTime
        // instance into a form proper for storage on the database tables using
        // the connection grammar's date format. We will auto set the values.
        elseif ($value && $this->isDateAttribute($key)) {
            $value = $this->fromDateTime($value);
        }

        if ($this->isJsonCastable($key) && ! is_null($value)) {
            $value = $this->castAttributeAsJson($key, $value);
        }

        // If this attribute contains a JSON ->, we'll set the proper value in the
        // attribute's underlying array. This takes care of properly nesting an
        // attribute in the array's value in the case of deeply nested items.
        if (Str::contains($key, '->')) {
            return $this->fillJsonAttribute($key, $value);
        }

        $this->attributes[$key] = $value;

        return $this;
    }
}

看了源码以后我们就很清楚了

定义的访问器是通过魔术方法来实现的,并不是真的会注册一个属性。

明白了以后,我们继续

第二次请求

调整代码

我们将controller function稍作修改

// CustomerController
public function testCustomer()
    try {
        $beginTime = microtime(true);
        /** @var Collection $customers */
        $customers = Customer::with('customerTags.tag')->select(['id'])->limit(15)->get();
        $customers->transform(function ($customer) {
            /** @var Customer $customer */
            $customer->test_tag = $customer->test_tag;
            return $customer;
        });
        $endTime = microtime(true);
        \Log::info($endTime - $beginTime);
        return responseSuccess($customers);
    } catch (\Exception $e) {
        errorLog($e);
        return responseFailed($e->getMessage());
    }
}

结果

日志中记录的时间为 local.INFO: 0.01134991645813

{
    "msg": "操作成功",
    "code": 200,
    "data": [
        {
            "id": 92424,
            "test_tag": "年龄太小/零基础/年龄过大",
            "customer_tags": [
                {
                    "id": 1586,
                    "customer_id": 92424,
                    "tag_id": 1,
                    "tag": {
                        "id": 1,
                        "name": "年龄太小",
                    }
                },
                {
                    "id": 1588,
                    "customer_id": 92424,
                    "tag_id": 2,
                    "tag": {
                        "id": 2,
                        "name": "零基础",
                    }
                },
                {
                    "id": 1587,
                    "customer_id": 92424,
                    "tag_id": 10,
                    "tag": {
                        "id": 10,
                        "name": "年龄过大",
                    }
                }
            ]
        },
        {
            "id": 16,
            "customer_tags": []
        },
        ...
    ]
}

OK,非常好,到这里我们已经实现了我们的需求。但是多余的 customer_tags 是前端不需要的,所以我们继续略改代码,将它移除掉。

第三次请求

调整代码

我们将controller function稍作修改,执行完访问器以后,删除掉 customer_tags

// CustomerController
public function testCustomer()
    try {
        $beginTime = microtime(true);
        /** @var Collection $customers */
        $customers = Customer::with('customerTags.tag')->select(['id'])->limit(15)->get();
        $customers->transform(function ($customer) {
            /** @var Customer $customer */
            $customer->test_tag = $customer->test_tag;
            unset($customer->customerTags);
            return $customer;
        });
        $endTime = microtime(true);
        \Log::info($endTime - $beginTime);
        return responseSuccess($customers);
    } catch (\Exception $e) {
        errorLog($e);
        return responseFailed($e->getMessage());
    }
}

结果

很奇怪,这里我们明明 unset() 移除了 $customer->customerTags, 结果还是返回了相关数据。

{
    "msg": "操作成功",
    "code": 200,
    "data": [
        {
            "id": 92424,
            "test_tag": "年龄太小/零基础/年龄过大",
            "customer_tags": [
                {
                    "id": 1586,
                    "customer_id": 92424,
                    "tag_id": 1,
                    "tag": {
                        "id": 1,
                        "name": "年龄太小",
                    }
                },
                {
                    "id": 1588,
                    "customer_id": 92424,
                    "tag_id": 2,
                    "tag": {
                        "id": 2,
                        "name": "零基础",
                    }
                },
                {
                    "id": 1587,
                    "customer_id": 92424,
                    "tag_id": 10,
                    "tag": {
                        "id": 10,
                        "name": "年龄过大",
                    }
                }
            ]
        },
        {
            "id": 16,
            "customer_tags": []
        },
        ...
    ]
}

很奇怪,这里我们明明 unset() 移除了 $customer->customerTags, 结果还是返回了相关数据。为什么呢?

这里我开启了 sql 日志以后,再次执行,依然还是之前的结果。没关系,我们不慌,来查看日志。

可以看出,在输出执行时间之后,又多出来了许多 sql ,而这些 sql 正是用来查询客户的tags相关信息的。执行时间输出以后就执行了 responseSuccess(),难道这个方法有问题?

让我们修改一下 responseSuccess(),添加一条 log

//对返回给前端前对数据进行格式处理
function responseSuccess($data = [], $message = '操作成功')
{
    $res = [
        'msg'  => $message,
        'code' => 200,
        'data' => $data
    ];
    //分页特殊处理
    if ($data instanceof Paginator) {
        $data = $data->toArray();
        $page = [
            'current_page' => $data['current_page'],
            'last_page'    => $data['last_page'],
            'per_page'     => $data['per_page'],
            'total'        => $data['total']
        ];

        $res['data']  = $data['data'];
        $res['pages'] = $page;
    }
    \Log::info('------------华丽的分割线-------------');
    return response()->json($res)->setStatusCode(200);
}

WTF ? 这是怎么回事?“华丽的分割线”之后就是框架提供的返回 json 数据的方法,难道框架本身出了什么问题?

追查 json() 方法

  1. tinker 中执行 response() 查看返回的对象
Psy Shell v0.9.9 (PHP 7.1.25 — cli) by Justin Hileman
>>> response()
=> Illuminate\Routing\ResponseFactory {#3470}
  1. 查看 Illuminate\Routing\ResponseFactory
namespace Illuminate\Routing;

use Illuminate\Http\JsonResponse;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\Routing\ResponseFactory as FactoryContract;

class ResponseFactory implements FactoryContract
{
    use Macroable;

        /**
     * Return a new JSON response from the application.
     *
     * @param  mixed  $data
     * @param  int  $status
     * @param  array  $headers
     * @param  int  $options
     * @return \Illuminate\Http\JsonResponse
     */
    public function json($data = [], $status = 200, array $headers = [], $options = 0)
    {
        return new JsonResponse($data, $status, $headers, $options);
    }
}
  1. 查看 Illuminate\Http\JsonResponse
namespace Illuminate\Http;

use JsonSerializable;
use InvalidArgumentException;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Arrayable;
use Symfony\Component\HttpFoundation\JsonResponse as BaseJsonResponse;

class JsonResponse extends BaseJsonResponse
{
    use ResponseTrait, Macroable {
        Macroable::__call as macroCall;
    }

    /**
     * Constructor.
     *
     * @param  mixed  $data
     * @param  int    $status
     * @param  array  $headers
     * @param  int    $options
     * @return void
     */
    public function __construct($data = null, $status = 200, $headers = [], $options = 0)
    {
        $this->encodingOptions = $options;

        parent::__construct($data, $status, $headers);
    }

    /**
     * {@inheritdoc}
     */
    public function setData($data = [])
    {
        $this->original = $data;

        if ($data instanceof Jsonable) {
            $this->data = $data->toJson($this->encodingOptions);
        } elseif ($data instanceof JsonSerializable) {
            $this->data = json_encode($data->jsonSerialize(), $this->encodingOptions);
        } elseif ($data instanceof Arrayable) {
            $this->data = json_encode($data->toArray(), $this->encodingOptions);
        } else {
            $this->data = json_encode($data, $this->encodingOptions);
        }

        if (! $this->hasValidJson(json_last_error())) {
            throw new InvalidArgumentException(json_last_error_msg());
        }

        return $this->update();
    }

        /**
     * Sets a raw string containing a JSON document to be sent.
     *
     * @param string $json
     *
     * @return $this
     *
     * @throws \InvalidArgumentException
     */
    public function setJson($json)
    {
        $this->data = $json;

        return $this->update();
    }
}
  1. 查看 Symfony\Component\HttpFoundation\JsonResponse 中的构造方法

在当前流程中,第四个参数一定是 false (调用的时候压根就没传第四个参数),所以就是调用了Illuminate\Http\JsonResponse::setData()

namespace Symfony\Component\HttpFoundation;

class JsonResponse extends Response
{
    /**
     * @param mixed $data    The response data
     * @param int   $status  The response status code
     * @param array $headers An array of response headers
     * @param bool  $json    If the data is already a JSON string
     */
    public function __construct($data = null, $status = 200, $headers = array(), $json = false)
    {
        parent::__construct('', $status, $headers);

        if (null === $data) {
            $data = new \ArrayObject();
        }

        $json ? $this->setJson($data) : $this->setData($data);
    }
}
  1. 分析 Illuminate\Http\JsonResponse::setData() 的执行
/**
* {@inheritdoc}
*/
public function setData($data = [])
{
    $this->original = $data;

    if ($data instanceof Jsonable) {
        $this->data = $data->toJson($this->encodingOptions);
    } elseif ($data instanceof JsonSerializable) {
        $this->data = json_encode($data->jsonSerialize(), $this->encodingOptions);
    } elseif ($data instanceof Arrayable) {
        $this->data = json_encode($data->toArray(), $this->encodingOptions);
    } else {
        $this->data = json_encode($data, $this->encodingOptions);
    }

    if (! $this->hasValidJson(json_last_error())) {
        throw new InvalidArgumentException(json_last_error_msg());
    }

    return $this->update();
}

通过看代码, 这么分支,那么是执行了哪个分支呢?所以我们要先弄清楚$data 的类型,$data 是什么呢?对 于$data ,一路传递过来,其实不难想明白,它就是我们一开始在responseSuccess() 中拼接的 $res ,然后 $res['data'] 是我们一开始查询得出的 $customers ,那我们都知道ORM 模型的结果集是Illuminate\Database\Eloquent\Collection。 所以这里 $data 作为一个数组,他会进入 setData() 中的第四个分支

$this->data = json_encode($data, \$this->encodingOptions);

详情请看这里 json_encode()如何转化一个对象?

json_encode() 是一个向下递归的遍历每一个可遍历的元素,如果遇到不可遍历元素是一个对象,则会判断对象是否实现了 JsonSerializable ,如果实现了 JsonSerializable ,则要看该对象的 jsonSerialize(),否则只会编码对象的公开非静态属性。

那我们看一下 $customers or Illuminate\Database\Eloquent\Collection 是否实现了 JsonSerializable

>>> $test = new \Illuminate\Database\Eloquent\Collection();
=> Illuminate\Database\Eloquent\Collection {#3518
     all: [],
   }
>>> $test instanceof JsonSerializable
=> true

Illuminate\Database\Eloquent\Collection 确实实现了 JsonSerializable,所以这里关于 $customers 被编码的情况,应该是要找到 Illuminate\Database\Eloquent\Collection 中的 jsonSerialize()

  1. 我们看一下 Illuminate\Database\Eloquent\Collection
namespace Illuminate\Database\Eloquent;

use LogicException;
use Illuminate\Support\Arr;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Support\Collection as BaseCollection;

class Collection extends BaseCollection implements QueueableCollection
{
        /**
     * Get the collection of items as a plain array.
     *
     * @return array
     */
    public function toArray()
    {
        return array_map(function ($value) {
            return $value instanceof Arrayable ? $value->toArray() : $value;
        }, $this->items);
    }

    /**
     * Convert the object into something JSON serializable.
     *
     * @return array
     */
    public function jsonSerialize()
    {
        return array_map(function ($value) {
            if ($value instanceof JsonSerializable) {
                return $value->jsonSerialize();
            } elseif ($value instanceof Jsonable) {
                return json_decode($value->toJson(), true);
            } elseif ($value instanceof Arrayable) {
                return $value->toArray();
            }

            return $value;
        }, $this->items);
    }

    /**
     * Get the collection of items as JSON.
     *
     * @param  int  $options
     * @return string
     */
    public function toJson($options = 0)
    {
        return json_encode($this->jsonSerialize(), $options);
    }
}

Illuminate\Database\Eloquent\Collection 又继承了Illuminate\Support\Collection

  1. 我们看一下 Illuminate\Support\Collection
namespace Illuminate\Support;

use stdClass;
use Countable;
use Exception;
use ArrayAccess;
use Traversable;
use ArrayIterator;
use CachingIterator;
use JsonSerializable;
use IteratorAggregate;
use Illuminate\Support\Debug\Dumper;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Arrayable;

class Collection implements ArrayAccess, Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable
{
    /**
     * The items contained in the collection.
     *
     * @var array
     */
    protected $items = [];
     /**
     * Create a new collection.
     *
     * @param  mixed  $items
     * @return void
     */
    public function __construct($items = [])
    {
        $this->items = $this->getArrayableItems($items);
    }
    /**
     * Results array of items from Collection or Arrayable.
     *
     * @param  mixed  $items
     * @return array
     */
    protected function getArrayableItems($items)
    {
        if (is_array($items)) {
            return $items;
        } elseif ($items instanceof self) {
            return $items->all();
        } elseif ($items instanceof Arrayable) {
            return $items->toArray();
        } elseif ($items instanceof Jsonable) {
            return json_decode($items->toJson(), true);
        } elseif ($items instanceof JsonSerializable) {
            return $items->jsonSerialize();
        } elseif ($items instanceof Traversable) {
            return iterator_to_array($items);
        }

        return (array) $items;
    }
}

可以看得出,Illuminate\Database\Eloquent\Collection 的父级 Illuminate\Support\Collection 实现了 JsonSerializable

看到这里,我们就已经很明白了。

$customers 会进入第一个分支,调用 $customers->jsonSerialize()

array_map() 中的回调函数会处理 $customers->items

array_map() 中的回调函数也有许多分支,依赖元素的类型来选择进入的分支

那么$customers->items 中的元素是什么呢?是 App\Models\Customer ,它继承了Illuminate\Database\Eloquent\Model

  1. 来看一下 Illuminate\Database\Eloquent\Model
namespace Illuminate\Database\Eloquent;

use Exception;
use ArrayAccess;
use JsonSerializable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Contracts\Queue\QueueableEntity;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\ConnectionResolverInterface as Resolver;

abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{
    use Concerns\HasAttributes,
        Concerns\HasEvents,
        Concerns\HasGlobalScopes,
        Concerns\HasRelationships,
        Concerns\HasTimestamps,
        Concerns\HidesAttributes,
        Concerns\GuardsAttributes;
        /**
     * Convert the model instance to an array.
     *
     * @return array
     */
    public function toArray()
    {
        return array_merge($this->attributesToArray(), $this->relationsToArray());
    }

    /**
     * Convert the model instance to JSON.
     *
     * @param  int  $options
     * @return string
     *
     * @throws \Illuminate\Database\Eloquent\JsonEncodingException
     */
    public function toJson($options = 0)
    {
        $json = json_encode($this->jsonSerialize(), $options);

        if (JSON_ERROR_NONE !== json_last_error()) {
            throw JsonEncodingException::forModel($this, json_last_error_msg());
        }

        return $json;
    }

    /**
     * Convert the object into something JSON serializable.
     *
     * @return array
     */
    public function jsonSerialize()
    {
        return $this->toArray();
    }
}

看到这里,我们就明白了。

  • $res 是一个数组,一路传递到 Illuminate\Http\JsonResponse::setData(),然后数组会被 json_encode() ,而 json_endode() 的本质就是遍历每一个元素进行编码。

  • $res['data'] 是一个 Illuminate\Database\Eloquent\Collection ,它实现了 JsonSerializable ,所以当遍历到它的时候,会调用 Illuminate\Database\Eloquent\Collection::jsonSerialize()

  • Illuminate\Database\Eloquent\Collection::jsonSerialize() 会遍历集合的属性 $items,而 $items 中的每一个元素又是一个 App\Models\Customer

  • App\Models\Customer 继承了Illuminate\Database\Eloquent\ModelIlluminate\Database\Eloquent\Model 也实现了 JsonSerializable ,所以会调用 Illuminate\Database\Eloquent\Model::jsonSerialize()

  • 通过查看 Illuminate\Database\Eloquent\Model 源码,我们发现,Illuminate\Database\Eloquent\Model 中的 toJson()jsonSerialize() 都是先调用了 toArray()

看来问题的关键就是 Illuminate\Database\Eloquent\Model::toArray()

public function toArray()
{
    return array_merge($this->attributesToArray(), $this->relationsToArray());
}
  1. 查看 $this->attributesToArray()

$this->relationsToArray() 是处理关联关系的,本质上还是对 CollectionModel 中的 toArray() 调用

namespace Illuminate\Database\Eloquent\Concerns;

use LogicException;
use DateTimeInterface;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection as BaseCollection;
use Illuminate\Database\Eloquent\JsonEncodingException;

trait HasAttributes
{
    /**
     * The model's attributes.
     *
     * @var array
     */
    protected $attributes = [];

    /**
     * The cache of the mutated attributes for each class.
     *
     * @var array
     */
    protected static $mutatorCache = [];

    /**
     * Convert the model's attributes to an array.
     *
     * @return array
     */
    public function attributesToArray()
    {
        // If an attribute is a date, we will cast it to a string after converting it
        // to a DateTime / Carbon instance. This is so we will get some consistent
        // formatting while accessing attributes vs. arraying / JSONing a model.
        // 日期处理相关
        $attributes = $this->addDateAttributesToArray(
            $attributes = $this->getArrayableAttributes()
        );

        //处理突变的方法 就是定义的访问器  $this->getMutatedAttributes() 就是正则获取定义的访问器名称
        $attributes = $this->addMutatedAttributesToArray(
            $attributes, $mutatedAttributes = $this->getMutatedAttributes()
        );

        // Next we will handle any casts that have been setup for this model and cast
        // the values to their appropriate type. If the attribute has a mutator we
        // will not perform the cast on those attributes to avoid any confusion.
        // 这个可以忽略,我们没有定义 $this->casts
        $attributes = $this->addCastAttributesToArray(
            $attributes, $mutatedAttributes
        );

        // Here we will grab all of the appended, calculated attributes to this model
        // as these attributes are not really in the attributes array, but are run
        // when we need to array or JSON the model for convenience to the coder.
        // 这个可以忽略,我们没有定义 $this->appends
        foreach ($this->getArrayableAppends() as $key) {
            $attributes[$key] = $this->mutateAttributeForArray($key, null);
        }

        return $attributes;
    }

    /**
     * Add the mutated attributes to the attributes array.
     * // 将一个突变的key及对应的值 添加到$attributes 如果key已经存在于$attributes,调用其对应的访问器
     *
     * @param  array  $attributes
     * @param  array  $mutatedAttributes
     * @return array
     */
    protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes)
    {
        foreach ($mutatedAttributes as $key) {
            // We want to spin through all the mutated attributes for this model and call
            // the mutator for the attribute. We cache off every mutated attributes so
            // we don't have to constantly check on attributes that actually change.
            // 如果key不存在于$attributes,跳过
            if (!array_key_exists($key, $attributes)) {
                continue;
            }

            // Next, we will call the mutator for this attribute so that we can get these
            // mutated attribute's actual values. After we finish mutating each of the
            // attributes we will return this final array of the mutated attributes.
            // 如果key存在于$attributes,就会调用这里的方法,注意传了一个值进去
            $attributes[$key] = $this->mutateAttributeForArray(
                $key, $attributes[$key]
            );
        }

        return $attributes;
    }

    /**
     * Get the value of an attribute using its mutator.
     * 调用访问器
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return mixed
     */
    protected function mutateAttribute($key, $value)
    {
        //调用访问器
        return $this->{'get'.Str::studly($key).'Attribute'}($value);
    }

    /**
     * Get the value of an attribute using its mutator for array conversion.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return mixed
     */
    protected function mutateAttributeForArray($key, $value)
    {
        //你没看错,$key已经存在于model的属性了,还是要继续调用了相应的访问器来执行一遍代码
        $value = $this->mutateAttribute($key, $value);
        //如果访问器返回的值实现了Arrayable,继续toArray()  (包含集合和模型)
        return $value instanceof Arrayable ? $value->toArray() : $value;
    }

    /**
     * Get the mutated attributes for a given instance.
     * 缓存访问器对应的key
     *
     * @return array
     */
    public function getMutatedAttributes()
    {
        $class = static::class;

        if (! isset(static::$mutatorCache[$class])) {
            static::cacheMutatedAttributes($class);
        }

        return static::$mutatorCache[$class];
    }

    /**
     * Extract and cache all the mutated attributes of a class.
     * 获取缓存访问器对应的key 转化成了下划线风格
     *
     * @param  string  $class
     * @return void
     */
    public static function cacheMutatedAttributes($class)
    {
        static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->map(function ($match) {
            return lcfirst(static::$snakeAttributes ? Str::snake($match) : $match);
        })->all();
    }

    /**
     * Get all of the attribute mutator methods.
     * 正则获取定义的访问器名称
     *
     * @param  mixed  $class
     * @return array
     */
    protected static function getMutatorMethods($class)
    {
        preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);

        return $matches[1];
    }
}

看的有点迷?

跑个代码看一下

>>> $c = Customer::query()->where('id',92424)->select(['id'])->first(92424);
=> App\Models\Customer {#3479
     id: 92424,
   }
>>> $c->test_tag = $c->test_tag;
=> "年龄太小/零基础/年龄过大"
>>> $c
=> App\Models\Customer {#3479
     id: 92424,
     test_tag: "年龄太小/零基础/年龄过大",
     customerTags: Illuminate\Database\Eloquent\Collection {#3487
       all: [
         App\Models\CustomerTag {#3498
           id: 1586,
           customer_id: 92424,
           tag_id: 1,
           tag: App\Models\Tag {#3504
             id: 1,
             name: "年龄太小",
           },
         },
         App\Models\CustomerTag {#3499
           id: 1588,
           customer_id: 92424,
           tag_id: 2,
           tag: App\Models\Tag {#211
             id: 2,
             name: "零基础",
           },
         },
         App\Models\CustomerTag {#3500
           id: 1587,
           customer_id: 92424,
           tag_id: 10,
           tag: App\Models\Tag {#3475
             id: 10,
             name: "年龄过大",
           },
         },
       ],
     },
   }
>>> $c->getMutatedAttributes()
=> [
     "test_tag",
   ]

可以看到,在执行 $c->test_tag = $c->test_tag; 以后 ,$c 中已经有了 test_tag 属性,test_tag 又是我们定义的访问器对应的,所以在 $this->addMutatedAttributesToArray() 中,对于已经存在于 Model 中的访问器属性,还是要继续调用相应的访问器来执行一遍代码。

  1. 回过头看一看我们写的代码
//App\Models\Customer
public function getTestTagAttribute()
{
    $customerTags = iteratorGet($this, 'customerTags', []);
    $tags         = [];
    foreach ($customerTags as $customerTag) {
        $tags[] = iteratorGet($customerTag->tag, 'name');
    }
    return implode('/', $tags);
}
// CustomerController
public function testCustomer()
    try {
        $beginTime = microtime(true);
        /** @var Collection $customers */
        $customers = Customer::with('customerTags.tag')->select(['id'])->limit(15)->get();
        $customers->transform(function ($customer) {
            /** @var Customer $customer */
            $customer->test_tag = $customer->test_tag;
            unset($customer->customerTags);
            return $customer;
        });
        $endTime = microtime(true);
        \Log::info($endTime - $beginTime);
        return responseSuccess($customers);
    } catch (\Exception $e) {
        errorLog($e);
        return responseFailed($e->getMessage());
    }
}

分析

由于我们为集合中的每一个模型都设置了 test_tag 属性,然后又删除了不想返回给前端的 relation 数据,那么根据上边对 laravel 源码的分析, 由于 test_tag 是我们定义的访问器对应的 key,并且 test_tag 被我们设置成了模型的属性,所以在将数据编码成为 json 的时候,访问器是一定会被触发的。然后关联关系会被重新查询出来,并且产生 sql

怎么样?惊喜不惊喜,意外不意外?

laravel 大法好,没想到还有这样的深坑等着我们吧?

有人会说,我看你的 responseSuccess() 有判断传进去的数据是否实现了分页器(Illuminate\Pagination\LengthAwarePaginator

分析分页器

分页器方法返回的结果集对象是 Illuminate\Pagination\LengthAwarePaginator

//Illuminate\Pagination\LengthAwarePaginator
<?php

namespace Illuminate\Pagination;

use Countable;
use ArrayAccess;
use JsonSerializable;
use IteratorAggregate;
use Illuminate\Support\Collection;
use Illuminate\Support\HtmlString;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Pagination\LengthAwarePaginator as LengthAwarePaginatorContract;

class LengthAwarePaginator extends AbstractPaginator implements Arrayable, ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Jsonable, LengthAwarePaginatorContract
{
    /**
     * The total number of items before slicing.
     *
     * @var int
     */
    protected $total;

    /**
     * The last available page.
     *
     * @var int
     */
    protected $lastPage;

    /**
     * Create a new paginator instance.
     *
     * @param  mixed  $items
     * @param  int  $total
     * @param  int  $perPage
     * @param  int|null  $currentPage
     * @param  array  $options (path, query, fragment, pageName)
     * @return void
     */
    public function __construct($items, $total, $perPage, $currentPage = null, array $options = [])
    {
        foreach ($options as $key => $value) {
            $this->{$key} = $value;
        }

        $this->total = $total;
        $this->perPage = $perPage;
        $this->lastPage = max((int) ceil($total / $perPage), 1);
        $this->path = $this->path !== '/' ? rtrim($this->path, '/') : $this->path;
        $this->currentPage = $this->setCurrentPage($currentPage, $this->pageName);
        $this->items = $items instanceof Collection ? $items : Collection::make($items);
    }

    /**
     * Get the instance as an array.
     *
     * @return array
     */
    public function toArray()
    {
        return [
            'current_page' => $this->currentPage(),
            'data' => $this->items->toArray(),
            'first_page_url' => $this->url(1),
            'from' => $this->firstItem(),
            'last_page' => $this->lastPage(),
            'last_page_url' => $this->url($this->lastPage()),
            'next_page_url' => $this->nextPageUrl(),
            'path' => $this->path,
            'per_page' => $this->perPage(),
            'prev_page_url' => $this->previousPageUrl(),
            'to' => $this->lastItem(),
            'total' => $this->total(),
        ];
    }

    /**
     * Convert the object into something JSON serializable.
     *
     * @return array
     */
    public function jsonSerialize()
    {
        return $this->toArray();
    }

    /**
     * Convert the object to its JSON representation.
     *
     * @param  int  $options
     * @return string
     */
    public function toJson($options = 0)
    {
        return json_encode($this->jsonSerialize(), $options);
    }
}

代码很容易理解,无论是 toJson(),还是 jsonSerialize() ,都是调用 toArray()

然后看构造方法可以明白 $this->items 就是集合(不是集合也转成集合了)

然后你一定特别明白'data' => $this->items->toArray(), 这一句,没错,调用了集合的 toArray()

所以分页器编码数据的最终方案还是会调用集合的 toArray() 来编码数据

怎么样?惊喜不惊喜,意外不意外?

laravel 大法好,没想到跳来跳去都会跳到同一个坑里吧?

解决方案

我们已经了解了访问器的坑是怎么产生的,那么针对性的解决方案其实并不难

方案一 换个毫不相干的属性名

换个毫不相干的属性名,懒人专属,不过不适合老项目,毕竟返回的字段名不是说改就能改的

修改代码

public function testCustomer()
{
    try {
        $beginTime = microtime(true);
        /** @var Collection $customers */
        $customers = Customer::query()->with('customerTags.tag')->orderByDesc('expired_at')->select(['id'])->paginate(5);
        $customers->transform(function ($customer) {
            /** @var Customer $customer */
            $customer->test_tag_info = $customer->test_tag;
            unset($customer->customerTags);
            return $customer;
        });
        $endTime = microtime(true);
        \Log::info($endTime - $beginTime);
        return responseSuccess($customers);
    } catch (\Exception $e) {
        errorLog($e);
        return responseFailed($e->getMessage());
    }
}

test_tag 改为 test_tag_info

结果

{
  "msg": "操作成功",
  "code": 200,
  "data": [
    {
      "id": 92424,
      "test_tag_info": "年龄太小/零基础/年龄过大"
    },
    {
      "id": 93863,
      "test_tag_info": "年龄太小"
    },
    {
      "id": 93855,
      "test_tag_info": "零基础"
    },
    {
      "id": 93852,
      "test_tag_info": "年龄太小"
    },
    {
      "id": 93797,
      "test_tag_info": ""
    }
  ]
}

可以看出并没有多余的数据返回

日志

可以看出并没有多余的 sql 产生

方案二 修改访问器

修改访问器?访问器还能怎么修改?

回想一下前边扒源码的时候,我有说过,在 $model 执行访问器的时候,有传一个值给到访问器,这个值就是访问器对应的 key$model->attributes 对应的值。

在调用访问器对应的 key 时,如果 key$model->attributes 中不存在,那么 $value 是一个 null

在编码转化 $model 时,如果 key$model->attributes 中不存在,那么该访问器不会被调用。

我们对传进访问器的值加以判断

修改代码

// App\Models\Customer
public function getTestTagAttribute($value)
{
    if ($value !== null) {
        return $value;
    }
    $customerTags = iteratorGet($this, 'customerTags', []);
    $tags         = [];
    foreach ($customerTags as $customerTag) {
        $tags[] = iteratorGet($customerTag->tag, 'name');
    }
    return implode('/', $tags);
}

在这里,我判断 $value 不为 null ,就返回 $value

结果

{
  "msg": "操作成功",
  "code": 200,
  "data": [
    {
      "id": 92424,
      "test_tag": "年龄太小/零基础/年龄过大"
    },
    {
      "id": 93863,
      "test_tag": "年龄太小"
    },
    {
      "id": 93855,
      "test_tag": "零基础"
    },
    {
      "id": 93852,
      "test_tag": "年龄太小"
    },
    {
      "id": 93797,
      "test_tag": ""
    }
  ]
}

可以看出并没有多余的数据返回

日志

可以看出并没有多余的 sql 产生,别说我拿上边的图,看下时间戳。

总结

laravel 确实是被大家认可的优秀的 php 框架

功能和特性十分丰富,对开发效率带来的提升确实不是一点半点,但是很多功能和特性,仅靠官方文档并不能真正了解怎么去用,怎么避开可能的坑。作为框架的使用者,我们不可能要求框架为我们而改变,我们能做的就是深入了解它,真正的驾驭它(吹牛皮的感觉真爽)

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
一冉再
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 16

可以考虑用 Arrayable 相关方法来 Set、Unset

推荐 Laravel Resources 用 toArray 实现.

5年前 评论
一冉再

@DamonTo 仔细看文章,就是 toArray() 产生的问题。

对于Model,无论是 toJson(),还是 jsonSerialize() ,都是调用 toArray()

ModeltoArray() 方法中,会先查出 Modelattributes,然后查出 Model 中所有的访问器及对应的 key,然后遍历这些 key,如果这个 key 已经存在于 attributes 中,访问器会重新执行一遍

5年前 评论
No_Panic

有使用过 league/fractaltransformer吗?
对返回的数据在加一层处理,而非通过getAttribute的方式

$books = [
    [
        'id' => '1',
        'title' => 'Hogfather',
        'yr' => '1998',
        'author_name' => 'Philip K Dick',
        'author_email' => 'philip@example.org',
    ],
    [
        'id' => '2',
        'title' => 'Game Of Kill Everyone',
        'yr' => '2014',
        'author_name' => 'George R. R. Satan',
        'author_email' => 'george@example.org',
    ]
];

// Pass this array (collection) into a resource, which will also have a "Transformer"
// This "Transformer" can be a callback or a new instance of a Transformer object
// We type hint for array, because each item in the $books var is an array
$resource = new Collection($books, function(array $book) {
    return [
        'id'      => (int) $book['id'],
        'title'   => $book['title'],
        'year'    => (int) $book['yr'],
        'author'  => [
            'name'  => $book['author_name'],
            'email' => $book['author_email'],
        ],
        'links'   => [
            [
                'rel' => 'self',
                'uri' => '/books/'.$book['id'],
            ]
        ]
    ];
});
5年前 评论
一冉再

@No_Panic 感谢,你的方案也不错

5年前 评论

那个customertag是多对多关系吧?多对多预加载速度还好吧!

5年前 评论
一冉再

@UpGod 问题不是在预加载。
ModeltoArray() 方法中,会先查出 Modelattributes,然后查出 Model 中所有的访问器及对应的 key,然后遍历这些 key,如果这个 key 已经存在于 attributes 中,访问器会重新执行一遍。
如果有预加载,肯定执行一遍也没问题,但是这些预加载的数据我不想返回给前端,unset() 了,所以 toArray() 再次执行访问器,会产生 N+1sql 问题

5年前 评论

好认真的分析,大概看了一下似乎是访问器覆盖原有属性,虽有又被隐藏的问题?印象中自己以前也踩过坑,但是最终好像还是有比较完美的解决方案的。一会儿仔细看一下作者的分析,点赞。

不过,由此问题引出的场景可见 N+1 问题多么普遍😂。

5年前 评论
一冉再

@Wi1dcard 分析源码,访问器可以接受参数,这个参数就是原有属性的值,可以做判断有接受的参数值,就返回这个参数值。没有才执行访问器的流程

5年前 评论

最喜欢大佬带我们读源码,这种感觉真的好,哈哈哈

5年前 评论
一冉再

@L学习不停 大佬不敢当,发现问题总是要解决的

5年前 评论

分析的非常详细!点赞先。

再说下个人看法,问题的关键应该是这个循环。

$customers->transform(function ($customer) {
/** @var Customer $customer */
$customer->test_tag = $customer->test_tag;  
return $customer;
});

个人觉得还可以有2个办法解决。

1.在customer模型使用追加属性 protected $appends = ['test_tag']; 这样上面的那个循环就可以删了。
2.设计数据库的时候如果使用多态多对多关联(保留customers表和tags表),移除customer_tags这张表也可以避免这种问题发生!

5年前 评论
一冉再

@fantasticcat 一样的,多态关联,我也要移除 tag。反正就是只返回 test_tagid两个字段,其他信息一概不返回。
关键 model 中有了 test_tag ,那么 test_tag 对应的访问器就还会执行。

5年前 评论

@一冉再 我去测试看看 = =!追加属性能解决吗

5年前 评论
一冉再

@fantasticcat 你可以在访问器中打印 log 测试,看看 log 输出几遍,就知道访问器会执行多少次了。
触发条件是 $model set一个属性对应该访问器

5年前 评论

@一冉再

用多态做了下测试结果如下!

table

  • users 字段 id,name
  • tags 字段 id,name
  • taggables 字段 tag_id,taggable_id,taggable_type

User模型

protected $appends = ['tags'];

public function tags()
{
    return $this->morphToMany(Tag::class, 'taggable');
}

public function getTagsAttribute()
{
    return implode('/', $this->tags()->pluck('name')->toArray());
}

Tag模型

public function users()
{
    return $this->morphedByMany(User::class, 'taggable');
}

public function articles()
{
    return $this->morphedByMany(Article::class, 'taggable');
}

路由

Route::get('/index', 'TestController@index');

Route::get('/index1','TestController@index1');

Route::get('/index2','TestController@index2');

TestController控制器

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class TestController extends Controller
{
    //设置getTagsAttribute方法和appends属性的做法
    public function index()
    {

        DB::enableQueryLog();

        DB::listen(function ($sql) {
            Log::info($sql->sql);
        });

        $beginTime = microtime(true);

        $users = User::get('id');

        $users = $users->toArray();  // 通过楼主的文章可知和response()->json($users); 是一样的道理

        //dd($users);

        $endTime = microtime(true);

        Log::info($endTime - $beginTime);

//        [2019-03-12 16:40:43] local.INFO: select `id` from `users`
//        [2019-03-12 16:40:43] local.INFO: select `name` from `tags` inner join `taggables` on `tags`.`id` = `taggables`.`tag_id` where `taggables`.`taggable_id` = ? and `taggables`.`taggable_type` = ?
//        [2019-03-12 16:40:43] local.INFO: select `name` from `tags` inner join `taggables` on `tags`.`id` = `taggables`.`tag_id` where `taggables`.`taggable_id` = ? and `taggables`.`taggable_type` = ?
//        [2019-03-12 16:40:43] local.INFO: select `name` from `tags` inner join `taggables` on `tags`.`id` = `taggables`.`tag_id` where `taggables`.`taggable_id` = ? and `taggables`.`taggable_type` = ?
//        [2019-03-12 16:40:43] local.INFO: 0.019001007080078

    }

    // 如果不设置getTagsAttribute和appends的做法,注释掉User模型的getTagsAttribute和appends属性
    public function index1(){

        DB::enableQueryLog();

        DB::listen(function ($sql) {
            Log::info($sql->sql);
        });

        $beginTime = microtime(true);

        // 这里不预加载,应该下面有循环
        $users = User::get('id');

        $users->transform(function ($user) {
            $user->tags = implode('/', $user->tags()->pluck('tags.name')->toArray());
            return $user;
        });

        $users = $users->toArray();  // 通过楼主的文章可知和response()->json($users); 是一样的道理

        //dd($users);

        $endTime = microtime(true);

        Log::info($endTime - $beginTime);

//        [2019-03-12 16:34:08] local.INFO: select `id` from `users`
//        [2019-03-12 16:34:08] local.INFO: select `tags`.`name` from `tags` inner join `taggables` on `tags`.`id` = `taggables`.`tag_id` where `taggables`.`taggable_id` = ? and `taggables`.`taggable_type` = ?
//        [2019-03-12 16:34:08] local.INFO: select `tags`.`name` from `tags` inner join `taggables` on `tags`.`id` = `taggables`.`tag_id` where `taggables`.`taggable_id` = ? and `taggables`.`taggable_type` = ?
//        [2019-03-12 16:34:08] local.INFO: select `tags`.`name` from `tags` inner join `taggables` on `tags`.`id` = `taggables`.`tag_id` where `taggables`.`taggable_id` = ? and `taggables`.`taggable_type` = ?
//        [2019-03-12 16:34:08] local.INFO: 0.025002002716064
    }

    // 如果不设置getTagsAttribute和appends的做法,注释掉User模型的getTagsAttribute方法和appends属性
    // 用laravel的集合完成需要实现的功能,而不是数据库
    public function index2()
    {
        DB::enableQueryLog();

        DB::listen(function ($sql) {
            Log::info($sql->sql);
        });

        $beginTime = microtime(true);

        // 这里预加载
        $users = User::with(['tags' => function ($query) {
            $query->select('tags.name');
        }])->get('id');

        $users = collect($users->toArray());

        $users->transform(function($user){
            $user['tags'] = implode('/',collect($user['tags'])->pluck('name')->toArray());
            return $user;
        });

        $users = $users->toArray();  // 通过楼主的文章可知和response()->json($users); 是一样的道理

        //dd($users);

        $endTime = microtime(true);

        Log::info($endTime - $beginTime);

//        [2019-03-12 16:33:07] local.INFO: select `id` from `users`
//        [2019-03-12 16:33:07] local.INFO: select `tags`.`name`, `taggables`.`taggable_id` as `pivot_taggable_id`, `taggables`.`tag_id` as `pivot_tag_id`, `taggables`.`taggable_type` as `pivot_taggable_type` from `tags` inner join `taggables` on `tags`.`id` = `taggables`.`tag_id` where `taggables`.`taggable_id` in (1, 2, 3) and `taggables`.`taggable_type` = ?
//        [2019-03-12 16:33:07] local.INFO: 0.025002002716064

    }

}

这个时间嘛~

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!