我第一次寻找所谓的 Laravel 框架的时候,个人其中一个目标就是要找:利用最简单的操做数据库的方法。后来目标就停在了 Eloquent ORM 上。php
今天说一说 Eloquent ORM 的一些不易被发现和使用的方法。laravel
平时这么写:sql
$article = Article::find($article_id); $article->read_count++; $article->save();
利用 increment
函数数据库
$article = Article::find($article_id); $article->increment('read_count');
固然能够传入数字,不仅是只增减 1:express
Article::find($article_id)->increment('read_count'); Article::find($article_id)->increment('read_count', 10); // +10 Product::find($produce_id)->decrement('stock'); // -1
咱们来看看源代码是怎么实现的:数组
/** * Increment a column's value by a given amount. * * @param string $column * @param int $amount * @param array $extra * @return int */ public function increment($column, $amount = 1, array $extra = []) { if (! is_numeric($amount)) { throw new InvalidArgumentException('Non-numeric value passed to increment method.'); } $wrapped = $this->grammar->wrap($column); $columns = array_merge([$column => $this->raw("$wrapped + $amount")], $extra); return $this->update($columns); } /** * Decrement a column's value by a given amount. * * @param string $column * @param int $amount * @param array $extra * @return int */ public function decrement($column, $amount = 1, array $extra = []) { if (! is_numeric($amount)) { throw new InvalidArgumentException('Non-numeric value passed to decrement method.'); } $wrapped = $this->grammar->wrap($column); $columns = array_merge([$column => $this->raw("$wrapped - $amount")], $extra); return $this->update($columns); }
主要利用 $this->grammar
解析 $column 字段,转变为可执行的 sql 语句。app
/** * Wrap a value in keyword identifiers. * * @param \Illuminate\Database\Query\Expression|string $value * @param bool $prefixAlias * @return string */ public function wrap($value, $prefixAlias = false) { if ($this->isExpression($value)) { return $this->getValue($value); } // If the value being wrapped has a column alias we will need to separate out // the pieces so we can wrap each of the segments of the expression on it // own, and then joins them both back together with the "as" connector. if (strpos(strtolower($value), ' as ') !== false) { return $this->wrapAliasedValue($value, $prefixAlias); } return $this->wrapSegments(explode('.', $value)); } /** * Wrap the given value segments. * * @param array $segments * @return string */ protected function wrapSegments($segments) { return collect($segments)->map(function ($segment, $key) use ($segments) { return $key == 0 && count($segments) > 1 ? $this->wrapTable($segment) : $this->wrapValue($segment); })->implode('.'); } /** * Wrap a single string in keyword identifiers. * * @param string $value * @return string */ protected function wrapValue($value) { if ($value !== '*') { return '"'.str_replace('"', '""', $value).'"'; } return $value; }
注: $grammer
是个抽象类,项目会根据不一样的数据库,而采用不一样的 $grammer
继承类来实现查询功能框架
最后一个参数是 $extra
,由于 increment
函数最后会执行 update()
方法,因此能够把额外须要操做数据的语句放在 $extra
数组中。ide
这里的 where
是前缀的做用,X
表示的是咱们的字段名,能够简化咱们的查询写法,平时都是这么写的:函数
$users = User::where('approved', 1)->get();
简便的写法:
$users = User::whereApproved(1)->get();
具体实现主要利用 __call
方法。
public mixed __call ( string $name , array $arguments )public static mixed __callStatic ( string $name , array $arguments )
在对象中调用一个不可访问方法时,__call() 会被调用。
在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。
在 Query/Builder.php
中能够看出:
/** * Handle dynamic method calls into the method. * * @param string $method * @param array $parameters * @return mixed * * @throws \BadMethodCallException */ public function __call($method, $parameters) { if (static::hasMacro($method)) { return $this->macroCall($method, $parameters); } if (Str::startsWith($method, 'where')) { return $this->dynamicWhere($method, $parameters); } $className = static::class; throw new BadMethodCallException("Call to undefined method {$className}::{$method}()"); }
where
查询方法都会调用函数:
return $this->dynamicWhere($method, $parameters);
/** * Handles dynamic "where" clauses to the query. * * @param string $method * @param string $parameters * @return $this */ public function dynamicWhere($method, $parameters) { $finder = substr($method, 5); $segments = preg_split( '/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE ); // The connector variable will determine which connector will be used for the // query condition. We will change it as we come across new boolean values // in the dynamic method strings, which could contain a number of these. $connector = 'and'; $index = 0; foreach ($segments as $segment) { // If the segment is not a boolean connector, we can assume it is a column's name // and we will add it to the query as a new constraint as a where clause, then // we can keep iterating through the dynamic method string's segments again. if ($segment !== 'And' && $segment !== 'Or') { $this->addDynamic($segment, $connector, $parameters, $index); $index++; } // Otherwise, we will store the connector so we know how the next where clause we // find in the query should be connected to the previous ones, meaning we will // have the proper boolean connector to connect the next where clause found. else { $connector = $segment; } } return $this; }
继续看 addDynamic
函数:
/** * Add a single dynamic where clause statement to the query. * * @param string $segment * @param string $connector * @param array $parameters * @param int $index * @return void */ protected function addDynamic($segment, $connector, $parameters, $index) { // Once we have parsed out the columns and formatted the boolean operators we // are ready to add it to this query as a where clause just like any other // clause on the query. Then we'll increment the parameter index values. $bool = strtolower($connector); $this->where(Str::snake($segment), '=', $parameters[$index], $bool); }
最后回到了 $this->where(Str::snake($segment), '=', $parameters[$index], $bool);
常规的 where
语句上;
同时,这过程咱们能够发现 whereX
方法,不只能够传入一个字段,并且还能够传入多个字段,用「And」或者 「Or」链接,且字段首字母用大写「A~Z」。
在平时有太多的写法都是,先查询,再判断是否存在,而后再决定是输出,仍是建立。
如:
$user = User::where('email', $email)->first(); if (!$user) { User::create([ 'email' => $email ]); }
一行代码解决:
$user = User::firstOrCreate(['email' => $email]);
注:这里还有一个函数 firstOrNew
和 firstOrCreate
类似,看代码:
/** * Get the first record matching the attributes or instantiate it. * * @param array $attributes * @param array $values * @return \Illuminate\Database\Eloquent\Model */ public function firstOrNew(array $attributes, array $values = []) { if (! is_null($instance = $this->where($attributes)->first())) { return $instance; } return $this->newModelInstance($attributes + $values); } /** * Get the first record matching the attributes or create it. * * @param array $attributes * @param array $values * @return \Illuminate\Database\Eloquent\Model */ public function firstOrCreate(array $attributes, array $values = []) { if (! is_null($instance = $this->where($attributes)->first())) { return $instance; } return tap($this->newModelInstance($attributes + $values), function ($instance) { $instance->save(); }); }
主要区别场景在于:若是是在已有 $attributes 下查找并建立的话,就能够用 firstOrCreate
。若是当咱们须要先查询而后再对 model 进行后续的操做的话,应使用 firstOrNew
方法,将 save 保存数据库操做放在最后;以避免重复执行 save()
方法。
find() 函数经过主键获取数据,平时都是获取单数据,其实传入的参数还能够是「主键数组」,获取多 models。
$users = User::find([1,2,3]);
咱们查看它的函数实现:
/** * Find a model by its primary key. * * @param mixed $id * @param array $columns * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|static[]|static|null */ public function find($id, $columns = ['*']) { if (is_array($id) || $id instanceof Arrayable) { return $this->findMany($id, $columns); } return $this->whereKey($id)->first($columns); }
首先判断的是 id 是否是 array,若是是的话,则执行 findMany 函数:
/** * Find multiple models by their primary keys. * * @param \Illuminate\Contracts\Support\Arrayable|array $ids * @param array $columns * @return \Illuminate\Database\Eloquent\Collection */ public function findMany($ids, $columns = ['*']) { if (empty($ids)) { return $this->model->newCollection(); } return $this->whereKey($ids)->get($columns); }
获取的结果是一个 Collection 类型。
Laravel 框架有不少地方值得咱们去研究,看 Laravel 是如何封装方法的。Eloquent ORM 还有不少方法能够一个个去看源代码是怎么实现的。
本文内容更多来自:https://laravel-news.com/eloquent-tips-tricks
还有不少函数均可以拿出来分析,如:
Relationship with conditions and ordering
public function orders() { return $this->hasMany('App\Order'); }
其实咱们能够在获取多订单的同时,加入筛选语句和排序。如,获取已支付并按更新时间倒序输出:
public function paidOrders() { return $this->hasMany('App\Order')->where('paid', 1)->orderBy('updated_at'); }
「未完待续」