75142913在线留言
【Laravel实战】3、文章内容展示:查询优化、缓存策略(实战干货)_PHP技术_网络人

【Laravel实战】3、文章内容展示:查询优化、缓存策略(实战干货)

Kwok 发表于:2022-04-11 17:11:01 点击:63 评论: 2

上一篇我们完成了用户注册与登陆,本文将接着进行下一步,关于文件发布与修改等操作,将统一放到后台,因为我的数据表里已存在了文章内容,所以,这里我仅做展示。

一、数据表结构

通过下面的语句创建文章数据表结构,因为存在与分类关联的情况,所以本文将与分类一起说明。

 1、文章标题及信息的表结构:

CREATE TABLE `meishi_articles` (
  `id` mediumint unsigned NOT NULL AUTO_INCREMENT,
  `subject` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章标题',
  `slug` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '自定义URL',
  `catid` smallint unsigned DEFAULT NULL COMMENT '分类ID',
  `user_id` mediumint unsigned DEFAULT NULL COMMENT '用户ID',
  `username` char(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名',
  `created_at` datetime DEFAULT NULL COMMENT '发表时间',
  `deleted_at` datetime DEFAULT NULL COMMENT '软删除',
  `updated_at` datetime DEFAULT NULL COMMENT '修改时间',
  `view_count` int unsigned NOT NULL DEFAULT '0' COMMENT '查看总数',
  `reply_count` mediumint unsigned NOT NULL DEFAULT '0' COMMENT '评论总数',
  `is_digest` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否精华:0否,1是',
  `toptime` datetime DEFAULT NULL COMMENT '置顶/站长推荐',
  `liked_count` int unsigned NOT NULL DEFAULT '0' COMMENT '用户点赞总数',
  `allow_reply` tinyint(1) NOT NULL DEFAULT '1' COMMENT '允许评论:0否,1是',
  `uuid` char(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '与其它表绑定',
  `cover_url` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '封面URL',
  `status` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '状态:0正常,1待审核',
  PRIMARY KEY (`id`),
  UNIQUE KEY `slug` (`slug`) USING BTREE COMMENT '自定义地址唯一性',
  KEY `catid` (`catid`) USING BTREE COMMENT '按分类查询',
  KEY `user_id` (`user_id`) USING BTREE COMMENT '按用户查询',
  KEY `created_at` (`created_at`) USING BTREE COMMENT '按发布时间排序',
  KEY `updated_at` (`updated_at`) USING BTREE COMMENT '按更新时间排序',
  KEY `status` (`status`) USING BTREE COMMENT '按文章状态查询',
  CONSTRAINT `article_catship` FOREIGN KEY (`catid`) REFERENCES `meishi_articles_categories` (`id`),
  CONSTRAINT `article_uidship` FOREIGN KEY (`user_id`) REFERENCES `meishi_users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='文章标题及信息';

文章信息表只记录与标题和文章属性相关的字段,并与分类、用户ID绑定以产生依赖关系。

2、文章内容表结构

CREATE TABLE `meishi_articles_contents` (
  `id` mediumint unsigned NOT NULL AUTO_INCREMENT,
  `article_id` mediumint unsigned NOT NULL DEFAULT '0' COMMENT '文章ID',
  `content` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章内容',
  `ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'IP v4/6',
  `page_order` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '分页排序',
  PRIMARY KEY (`id`),
  KEY `article_id` (`article_id`) USING BTREE COMMENT '按文章id索引',
  CONSTRAINT `article_content_idship` FOREIGN KEY (`article_id`) REFERENCES `meishi_articles` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='文章内容及分页';

文章内容记录分页、发布者IP等内容,并与文章ID绑定,一篇文章可以有多个分页内容,按page_order排序。

 

上面所有的数据表都做了简单的索引优化,也可以根据自己的情况增删索引。

注意:因为存在表关联的情况,如果出现建表报错的情况下可以忽略外键约束。

小技巧:我原来的表存的是时间戳,可以使用下面的命令转换为datatime:

UPDATE `meishi_articles` SET `created_at` = FROM_UNIXTIME(`created_at`);
ALTER TABLE `meishi_articles` CHANGE `created_at ` `created_at ` DATETIME NULL DEFAULT NULL COMMENT '发表时间';

UPDATE `meishi_articles` SET `updated_at` = FROM_UNIXTIME(`updated_at`);
ALTER TABLE `meishi_articles` CHANGE `updated_at` `updated_at` DATETIME NULL DEFAULT NULL COMMENT '修改时间';

二、文章的控制器与模型

1、文章显示控制器

因为我希望将文章的显示与读写分开,所以我单独make一个单动作控制器:

php artisan make:controller article/ArticleShowController --invokable#注册文章单动作控制器

此命令会生成文件:app/Http/Controllers/article/ArticleShowController.php,内容如下:

<?php
namespace AppHttpControllersarticle;
use AppHttpControllersController;
use IlluminateHttpRequest;
use IlluminateSupportFacadesCache; //使用缓存
use AppModelsarticleArticle; //文章模型
use IlluminateSupportFacadesAuth; //验证用户
class ArticleShowController extends Controller
{

    private $id; //文章 ID
    private $slug; //文章 slug
    private $page = 1; //文章分页
    /**
     * 单动作控制器
     *
     * @param  IlluminateHttpRequest  $request
     * @return IlluminateHttpResponse
     */
    public function __invoke(Request $request)
    {
        //判断文章使用的是slug还是ID访问
        if (is_numeric($request->id)) {
            $this->id = $request->id; //通过文章ID访问            
        } else {
            $this->slug = $request->slug; //通过自定义URL访问
        }

        $this->page = ($request->page ?? 1) - 1; //当前文件分页
        $duration = 1; //文章缓存时间
        $data = Cache::remember($this->get_cache_key($this->id), $duration, function () {
            return [
                'article' => $this->getArticlTitle(), //获取文章标题及信息
                'content' => $this->getArticleContents(), //获取文章所有内容分页
                'tags' => $this->getTags(), //文章相关的Tags
                'comments' => $this->getComments(10), //10条文章评论
            ];
        });
        $data['content'] = $data['content']->get($this->page) ?? abort(404, '文章分页不存在'); //只需要当前分页
        return view('article.page', $data); //将数据交给视图处理
    }

    //获取文章标题及信息
    private function getArticlTitle()
    {
        $article = $this->slug
            ? Article::withoutGlobalScope('status')->where('slug', $this->slug)->first()
            : Article::withoutGlobalScope('status')->find($this->id);
        $this->id = $article->id ?? abort(404, '文章不存在'); //文章ID
        //  dump(Auth::user()->isAdmin());
        //管理员可见
        abort_unless($article->status != 0 && Auth::user()->isAdmin(), 403, '文章审核中');
        return $article;
    }

    // 说明: 获取当前文章的所有分页    
    private function getArticleContents()
    {
        return Article::withoutGlobalScope('status')->find($this->id)->contents; //一对多关联查询
    }

    //说明: 获取缓存key
    private function get_cache_key($id)
    {
        return 'article_' . $id;
    }
}

 控制器里的代码知识点:

1、使用了缓存Cache::remember 闭包的方式优先读取缓存里的内容,当缓存不存在或者过期时才调用相关方法查询数据库。

2、使用了几个辅助函数(abort_unless、abort_if,abort)快速的检测文章状态是否正常并处理。

3、因为我们Article模型里使用了字段全局作用域,为了显示审核中的文章,我们使用了withoutGlobalScope忽略限制。

4、使用了集合里的->get()方法获取到内容的分页内容(所有分页都缓存到当前文章里的)。 

2、文章模型

然后创建文章Model。

php artisan make:model article/Article #文章模型

此命令会生成文件:app/Models/article/Article.php

<?php

namespace AppModelsarticle;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentBuilder; //匿名全局作用域
use IlluminateDatabaseEloquentCastsAttribute; //属性修改器

class Article extends Model
{
    use HasFactory;

    //需要强制转换的字段
    protected $casts = [
        'toptime' => 'datetime:Y-m-d H:i:s',
        'created_at' => 'datetime:Y-m-d H:i:s', //转为中国人习惯
        'updated_at' => 'datetime:Y-m-d H:i:s',
        'allow_reply' => 'boolean', //是否允许回复
        'is_digest' => 'boolean', //是否精华
    ];
    //追加字段(虚拟字段访问器里的内容)
    protected $appends = ['description'];
    //虚拟字段访问器
    public function getDescriptionAttribute()
    {
        return $this->attributes['subject'] . ' 本文章的描述';
    }
    //使用匿名全局作用域:默认应用于所有模型调用查询
    //移除此限制请使用:Article::withoutGlobalScope('status')->find(1);
    //全局作用域还可以使用类的方式,请参考手册,这里使用快捷的闭包方式
    protected static function booted()
    {
        //使用闭包方式创建匿名全局作用域
        static::addGlobalScope('status', function (Builder $builder) {
            $builder->where('status', 0); //限制文章状态
        });
    }

    // 说明: 仅作为演示的查询封装,默认为状态正常的文章
    // 使用:Article::Normal(1)->find($id);来显示待审核的文章
    // 本地作用域可以通过$status参数显示文章状态为0,1,2的但会和上面的全局作用域冲突
    public function scopeNormal($query, int $status = 0)
    {
        return $query->where('status', $status);
    }

    //封面字段处理(属性修改器)
    protected function coverUrl(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => asset($value), //查询器: 返回完整的封面路由
            set: fn ($value) => strtolower($value), //修改器:写入时全部转为小写
        );
    }

    //获取文章分页内容
    public function contents()
    {
        return $this->hasMany(ArticleContent::class); //查询到多个分页,以数组方式返回
    }
}

文章模型知识点:

  1. 使用 protected $casts 将我们使用的时间转换为国人习惯。
  2. 转换数据库里的 0 为false,1 为 true.
  3. 使用了闭包的方式创建了一个匿名全局作用域
  4. 演示了本地作用域
  5. 演示了属性修改器
  6. 使用一对多的方法获取了文章的分页内容

 3、文章内容模型

在上面的文章模型里可以看到$this->hasMany(ArticleContent::class)代码,使用了一对多的查询方式获取到文章的内容,所以我们需要新建一个文章内容模型以方便使用:

php artisan make:model article/ArticleContent #文章内容模型

三、注册文章路由

文件查看是一个单动作控制器,所以我们可以不指定调用的方法。

//文章可以使用slug自定义的路径
Route::get('/article/{id}', AppHttpControllersarticleArticleShowController::class);

路由并未校验ID是否为数字,因为我们还可能使用slug路径访问文章内容。

四、SQL查询性能优化

Eloquent默认情况下使用的是 select * 方式查询数据库的,我们可以选择需要的字段,根据 http://www.55mx.com/php/208.html 最后推荐的方式选择要查询的字段:

1、标题信息查询优化:

$columns = ['id', 'subject', 'style', 'slug', 'catid', 'user_id', 'username', 'created_at', 'updated_at', 'toptime', 'view_count', 'reply_count', 'is_digest', 'liked_count', 'allow_reply', 'cover_url', 'status'];

//哪种路径访问
$article = $this->slug ?
           Article::withoutGlobalScope('status')->where('slug', $this->slug)->first($columns) :
           Article::withoutGlobalScope('status')->where('id', $this->id)->first($columns);
   

将生成下面的语句:

select `id`, `subject`, `style`, `slug`, `catid`, `user_id`, `username`, `created_at`, `updated_at`, `toptime`, `view_count`, `reply_count`, `is_digest`, `liked_count`, `allow_reply`, `cover_url`, `status` from `meishi_articles` where `id` = ? limit 1

 2、文件内容页查询优化

此项需要通过Article模型里的 function contents 一对多关联查询的后面增加select方法即可。

return $this->hasMany(ArticleContent::class)->select('content', 'page_order'); //查询到多个分页,以数组方式返回

细心的小伙伴可能发现了一个小问题,我们分页没有排序,当然我们可以在后面追回方法:->orderBy('page_order', 'asc');按分页排序,这将消耗Msql服务器资源,如果你希望消耗WEB服务器资源可以看下面的操作:

$contents = Article::withoutGlobalScope('status')->find($this->id)->contents; //一对多关联查询
return $contents->sortBy('page_order'); //使用Laravel集合排序

MySQL也是先查询后再排序的,因为数据库是底层优化过的,效率也许会比laravel高,但数据量不大的情况下,几乎没有差别。这里只做一个解决方案实现,可以根据自己的情况选择~ 

五、缓存使用 

在上面的例子里我们通过Cache::remember配合闭包的方式优雅的实现了缓存,其实这只是解决方案之一,我们还有很多种可使用的方式,这里做为扩展了解一下。

1、参考缓存策略一:写入到数据库里就缓存

在writeThrough策略中,缓存服务器位于请求和数据库服务器之间,使每个写入操作在进入数据库服务器之前都经过缓存服务器。因此,writeThrough缓存策略与readThrough策略相似。

您可以使用以下代码使用Laravel缓存实现此策略:

public function writeThrough($key, $data, $minutes) {
    $cacheData = Cache::put($key, $data, $minutes);//缓存数据

    //数据库服务器(在缓存服务器之后)被调用。
    $this->storeToDB($cachedData);//存到数据库
    return $cacheData//返回缓存数据
}

//存储到数据库
private function storeToDB($data){
    Database::create($data);//写入到数据库
    return true
}

2、参考缓存策略二:回写

该策略是通过添加写入操作延迟来实现写入策略的更高级方法。

您还可以称之为writeBehind策略,因为在将数据写入数据库服务器之前,缓存服务器会延迟时间。

您可以使用以下代码使用Laravel缓存实现此策略:

$durationToFlush = 1; // (in minute)
$tempDataToFlush = [];

  public function writeBack($key, $data, $minutes){
    return $this->writeThrough($key, $data, $minutes);
  }

  public function writeThrough($key, $data, $minutes) {
      $cacheData = Cache::put($key, $data, $minutes);
      $this->storeForUpdates($cacheData);
      return $cacheData;
  }

// 存储新数据临时数组更新
  private function storeForUpdates($data){
    $tempData = {};
    $tempData['duration'] = this.getMinutesInMilli();
    $tempData['data'] = data;
    array_push($tempDataToFlush, data);
  }

// 将分钟转换为毫秒
private function getMinutesInMilli(){
  $currentDate = now();
  $futureDate = Carbon(Carbon::now()->timestamp + $this->durationToFlush * 60000)
  return $futureDate->timestamp
}

// 更新数据库服务器的调用。
public function updateDatabaseServer(){
  if($this->tempDataToFlush){
    foreach($this->tempDataToFlush as $index => $obj){
      if($obj->duration timestamp){
        if(Database::create($obj->data)){
            array_splice($this->tempDataToFlush, $index, 1);
        }
      }
    }
  }
}

writeBack方法调用writeThrough方法,该方法将数据存储到缓存服务器,并稍后使用updateDatabaseServer方法将临时数组推送到数据库服务器。您可以设置CronJob,每五分钟更新一次数据库服务器。

3、参考缓存三:写周围

此策略允许所有写入操作直接进入数据库服务器,而无需更新缓存服务器——只有在读取操作期间,缓存服务器才会更新。

假设用户想要创建新文章,文章将直接存储到数据库服务器。当用户想首次阅读文章的内容时,将从数据库服务器检索文章,并更新缓存服务器以供后续请求。

您可以使用以下代码使用Laravel缓存实现此策略:

public function writeAround($data) {
    $storedData = Database::create($data);
    return $storedData;
}

public function readOperation($key, $minutes){
    $cacheData = Cache::remember($key, $minutes, function() {
      return Article::all();
    })
    return $cacheData;
}

 4、参考缓存策略四:懒加载(常用方案)

数据库在这个策略中处于一边,应用程序首先从缓存服务器请求数据。然后,如果有命中(找到),数据将返回给客户端。否则,如果出现错误(未找到),数据库服务器会请求数据,并为后续请求更新缓存服务器。

public function lazyLoadingStrategy($key, $minutes, $callback) {
  if (Cache::has($key)) {
      $data = Cache::get($key);
      return $data;
  } else {
      //在缓存服务器之外调用数据库服务器。
      $data = $callback();
      Cache::set($key, $data, $minutes);
      return $data;
  }
}

上面的代码显示了缓存Aside策略的实现,这相当于实现Cache::remember方法。

5、参考缓存策略五:通读

此策略与缓存搁置策略直接相反。在这个策略中,缓存服务器位于客户端请求和数据库服务器之间。

请求直接发送到缓存服务器,如果缓存服务器中找不到数据,缓存服务器负责从数据库服务器检索数据。

您可以使用以下代码使用Laravel缓存实现此策略:

public function readThrough($key, $minutes) {
      $data = Cache::find($key, $minutes);
      return $data;
}

private function find($key, $minutes){
    if(Cache::has($key);){
      return Cache::get($key);
    }
    // 从缓存服务器调用数据库服务器。
    $DBdata = Database::find($key);
    Cache:put($key, $DBdata, $minutes);
    return $DBdata;
}

6、缓存Laravel应用程序的UI部分

缓存我们Laravel应用程序的用户界面是一个被称为全页缓存FPC的概念。该术语指的是从应用程序缓存HTML响应的过程。

它非常适合动态HTML数据不经常更改的应用程序。您可以缓存HTML响应,以获得更快的整体响应和HTML渲染(HTML直出,效率直逼HTML文件)。

我们可以使用以下代码行实现FPC:

class ArticlesController extends Controller {
    public function index() {
        if ( Cache::has('articles_index') ) {
            return Cache::get('articles_index');
        } else {
            $news = News::all();
            $cachedData = view('articles.index')->with('articles', $news)->render();
            Cache::put('articles_index', $cachedData);                                         
            return $cachedData;           
        }  
    }
}

乍一看,您可能已经注意到,我们检查了该文章索引页面是否已经存在于我们的缓存服务器中。然后,我们使用Laravel的view()和render()方法渲染页面。

否则,在将渲染的页面返回浏览器之前,我们会渲染页面并将输出存储在缓存服务器中,以进行后续请求。

除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/214
标签:laravel实战文章分类文章内容Kwok最后编辑于:2022-04-13 15:13:35
0
感谢打赏!

《【Laravel实战】3、文章内容展示:查询优化、缓存策略(实战干货)》的网友评论(2)

  1. #1哈哈,基金亏钱了,原来域名给出售了。。
    admin于2年前(2022-04-20)发表。(0)(0)
  2. #2可恶neter8搬家了 找了一个小时才找到 吓死了
    佚名于2年前(2022-04-15)发表。(0)(0)

本站推荐阅读

热门点击文章