上一篇我们完成了用户注册与登陆,本文将接着进行下一步,关于文件发布与修改等操作,将统一放到后台,因为我的数据表里已存在了文章内容,所以,这里我仅做展示。
通过下面的语句创建文章数据表结构,因为存在与分类关联的情况,所以本文将与分类一起说明。
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绑定以产生依赖关系。
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 '修改时间';
因为我希望将文章的显示与读写分开,所以我单独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()方法获取到内容的分页内容(所有分页都缓存到当前文章里的)。
然后创建文章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); //查询到多个分页,以数组方式返回
}
}
文章模型知识点:
在上面的文章模型里可以看到$this->hasMany(ArticleContent::class)代码,使用了一对多的查询方式获取到文章的内容,所以我们需要新建一个文章内容模型以方便使用:
php artisan make:model article/ArticleContent #文章内容模型
文件查看是一个单动作控制器,所以我们可以不指定调用的方法。
//文章可以使用slug自定义的路径
Route::get('/article/{id}', AppHttpControllersarticleArticleShowController::class);
路由并未校验ID是否为数字,因为我们还可能使用slug路径访问文章内容。
Eloquent默认情况下使用的是 select * 方式查询数据库的,我们可以选择需要的字段,根据 http://www.55mx.com/php/208.html 最后推荐的方式选择要查询的字段:
$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
此项需要通过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配合闭包的方式优雅的实现了缓存,其实这只是解决方案之一,我们还有很多种可使用的方式,这里做为扩展了解一下。
在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
}
该策略是通过添加写入操作延迟来实现写入策略的更高级方法。
您还可以称之为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,每五分钟更新一次数据库服务器。
此策略允许所有写入操作直接进入数据库服务器,而无需更新缓存服务器——只有在读取操作期间,缓存服务器才会更新。
假设用户想要创建新文章,文章将直接存储到数据库服务器。当用户想首次阅读文章的内容时,将从数据库服务器检索文章,并更新缓存服务器以供后续请求。
您可以使用以下代码使用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;
}
数据库在这个策略中处于一边,应用程序首先从缓存服务器请求数据。然后,如果有命中(找到),数据将返回给客户端。否则,如果出现错误(未找到),数据库服务器会请求数据,并为后续请求更新缓存服务器。
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方法。
此策略与缓存搁置策略直接相反。在这个策略中,缓存服务器位于客户端请求和数据库服务器之间。
请求直接发送到缓存服务器,如果缓存服务器中找不到数据,缓存服务器负责从数据库服务器检索数据。
您可以使用以下代码使用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;
}
缓存我们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实战】3、文章内容展示:查询优化、缓存策略(实战干货)》的网友评论(2)