dream

一个菜鸟程序员的成长历程

0%

mysql事务怎么实现的,什么是两阶段提交

我们都知道使用mysql的事务,准确来说是innoDB引擎的事务,可以保证数据的一致性,原子性等。那么为什么呢?

redo log

innoDB里面用到了一个叫做redo log(重做日志)的东西。

当你进行数据库操作的时候,innoDB并不会直接操作磁盘数据,因为这样很慢很慢。它使用了wal的机制,所有的操作先记录在redo log。等没事干了或者redo log满了再把数据刷到磁盘里面。这样的话他刷盘的时候就是顺序io,避免了使用随机io。redo log的大小我们可以通过参数设置。

缓冲池

在innoDB里面还有一个缓冲池,buffer_pool,这个缓冲池是在内存中的。为啥有缓冲池,因为磁盘的速度太慢了,如果每次都直接操作磁盘,那就完犊子了。就需要用到内存来缓冲一下,提升性能。

从innoDB里面读取数据的时候,实际上读取的是缓冲池,只有当缓冲池中没有记录的时候,才会读取磁盘,并且将记录放入缓冲池方便下次读取。

缓冲池的大小可以通过innodb_buffer_pool_size参数来指定。

缓存池中基本有以下组件:

  • 索引页
  • 数据页
  • undo页
  • 插入缓冲(insert buffer)
  • 自适应哈希索引
  • 锁信息
  • 数据字典
  • redo log buffer

缓冲池既然是在内存中,有固定大小,就会有淘汰机制,缓冲池使用的是LRU机制,也就是最近最少使用的会被淘汰掉。

这里面还有一个redo log buffer,这里面存储的就是上面说的redo log的信息了,会先写入这个buffer,然后再刷入redo log里面。有一个参数innodb_log_buffer_size来控制redo log buffer的大小。这个不用太大,因为一般每秒都会把这里面的刷新到redo log。

redo log buffer刷新到redo log的三种情况:

  • 每秒刷新
  • 事务提交时候刷新
  • 当redo log buffer剩余空间少于一半的时候刷新

binlog

redo log是innoDB引擎的日志,那mysql自己同样有日志,就是这个binlog日志。binlog 里面记载了mysql的所有变化,可以用来恢复数据库,创建备库,从库等。可以查看我的mysql读写分离

那为什么有两个日志呢,这是因为mysql最开始默认使用的是myisam引擎,myisam引擎没有redo log,mysql就有自己的binlog。innoDB引擎是后来加入mysql的。所以他们各自有各自的日志。

这两个日志有什么区别呢?

  1. rodo log是innoDB引擎的,binlog 是mysql的,binlog所有引擎都可以使用。
  2. redo log记录的是“再某个数据页上修改了啥”,binlog记录的是你的sql。
  3. redo log是固定大小,循环写入的,binlog没有大小限制,不断追加。

事务

那一个事务怎么执行的呢,比如更新一个登录时间。

1
update user set login_time = ? where id = 1;
  • 走完了前面的连接器,分析器,优化器,执行器。执行器会去innoDB更新数据了。
  • 他会先取出这条数据,取出id = 1的数据,id是主键索引,和数据在一起。innoDB直接查找主键索引树找到对应数据就返回了。当然,如果数据再缓冲池,那么直接返回了。
  • 执行器拿到以后再修改登陆时间,然后再次调用innoDB引擎的接口。
  • innoDB把数据更新到内存中,同时更新redo log,把redo log标记成待提交状态。
  • 执行器写入binlog。
  • 执行器调用innoDB引擎的接口,innoDB把redo log标记成提交状态。

这个时候整个更新事务完成。

为什么要这么做呢?

因为有两个日志的原因,所以两个日志都要写入,要保证这两个日志的一致性。那么如果不使用两阶段提交的方式,直接写入redo log然后写入binlog有什么问题呢?

假设,写完redo log,系统挂了。那么重启后innoDB引擎会根据redo log日志来恢复数据库。这时候数据库里面的数据是正确的。但是binlog丢失了啊。如果你有从库,那么从库的数据就错误了。因为从库的数据是通过binlog同步的。

如果把这两个步骤反过来呢,先写入binlog 再写入redo log呢?

那么就会redo log丢失,数据库实际上没有更新。但是从库通过binlog更新了。还是数据不一致。

所以需要两阶段提交来保证数据一致性。如果这时候写完redo log后挂掉了,因为redo log和binlog都没有数据,所以会回滚事务。
如果binlog和redo log都写入了,但是没有提交,那么重启后会提交事务。这样binlog和数据库就都有数据了。

一条mysql语句的奇幻之旅

我们平常都会用到mysql,但是你真的知道sql语句是怎么执行的吗?

比如下面的sql

1
select * from users;

这条sql语句大致会经过下面的阶段:

  1. 连接器 – 连接器负责建立,管理mysql的连接,你首先要连接到mysql服务器,才能操作mysql。
  2. 分析器 – 分析器会进行词法分析和语法分析,语法错误会在这个阶段被返回。
  3. 优化器 – 优化器会自动优化我们的sql语句,如果它发现你的sql语句可以进行优化的话。优化器选择是否使用索引,索引怎么用。优化器还会选择更优的执行计划,最后会输出一个执行计划。
  4. 执行器 – 执行器会根据输出的执行计划进行最终的执行。他会调用底层存储引擎的接口,比如innoDB引擎。

当我们最开始连接数据库实例的时候,我们要输入用户名密码,这时候连接器会从数据库的用户信息中判断你是否有权限连接数据库进行操作,有哪些权限。

如果你输入的用户名密码错误或者没有权限,那么你会收到下面的报错信息

1
Access denied for user 'root'@'localhost'(using password: YES)

这里我们要搞清楚一个概念,数据库和数据库实例,我们使用的都是数据库实例,用实例来操作数据库。

连接成功以后开始执行上面的查询语句。分析器会分析这个语句的词法,语法,语义这些信息。通俗来讲就是看到select,update这些关键字,知道你要来干啥,看看你是不是来搞破坏的,来捣蛋的。看看你是查询哪个表啊,有什么条件啊,这些玩意。最后会输出一个词法树。当然了这一步还会分析你的语法有没有错误,比如你把select打错试试。打成elect,会出现下面的报错信息

1
You have an error in your SQL syntax: check the maual that corresponds to your MySQL server version for the right syntax to use near 'elect * from users' at line 1

遇到这种sql语法错误,他会告诉你在哪出现了错误,其实就是分析器往后分析的时候发现,这不对啊,这不是我要的东西啊,你这偷梁换柱啊。

当这一步的考验你也通过了,那么骚年,你离飞升不远了。。。

进入优化器阶段,优化器接过语法树,会调整你的语法,比如他觉得你这么写它执行起来慢,执行的不爽,那么他会换个姿势再来一次。包括你的语法,执行的顺序,使用的索引,连表怎么连。最终优化完成会输出一个执行计划。

这个执行计划我们可以通过explain关键字查看,比如执行下面的语句

1
2

explain select * from users;

他会输出下面的信息。我们可以看到type这一列是all。这代表全表扫描。

Image text

我们现在在执行另外一个看看。

1
2
3

explain select * from users where id = 1;

看看这个执行结果。可以看到type这一列是const。这代表是常量。还有key这一列是primary,这代表我们这次使用了主键索引。而主键索引是聚簇索引,他的page页里面存储的就是这一行信息,所以只会查询一次。

Image text

上面输出的执行计划会最终由执行器来执行,执行器会调用存储引擎层的接口。

缓存

其实,在这中间还有一个缓存组件。最开始查询的时候会先查询缓存组件的信息,如果没有缓存,才会走到分析器,然后往下走。如果命中缓存,那么直接返回,也就没后面什么事情了。

laravel-react实战打造企业级高并发分布式电商小程序(三)–权限管理的前端

react

我们前端使用react来做,我们这个后台基于antd pro。使用npm创建它。

npm create umi

选择 ant-design-pro


Select the boilerplate type (Use arrow keys)
❯ ant-design-pro - Create project with an layout-only ant-design-pro boilerplate, use together with umi block.
app - Create project with a simple boilerplate, support typescript.
block - Create a umi block.
library - Create a library with umi.
plugin - Create a umi plugin.


Ant Design Pro 脚手架将会自动安装。

本地开发

npm install
npm start

启动完成后会自动打开浏览器访问 http://localhost:8000,你看到下面的页面就代表成功了。

Image text

vscode无法执行npm等脚本的问题

这是因为在windows系统上面powershell的执行策略问题。

可以运行下面的命令查看当前执行策略

Get-ExecutionPolicy

使用下面的命令更改执行策略

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

-scope参数是 限制在当前用户下面更改 策略更改为RemoteSigned策略。

RemoteSigned
  • Windows服务器计算机的默认执行策略。
  • 脚本可以运行。
  • 需要从可信任的发布者处获得从互联网下载的脚本和配置文件的数字签名,其中包括电子邮件和即时消息传递程序。
  • 不需要在本地计算机上编写且未从Internet下载的脚本上进行数字签名。
  • 如果脚本不受阻碍(例如使用Unblock-Filecmdlet),则运行从Internet下载且未签名的脚本。
  • 可能会运行来自Internet以外的其他来源的未签名脚本和可能有害的已签名脚本。
Restricted
  • Windows客户端计算机的默认执行策略。
  • 允许使用单个命令,但不允许使用脚本。
  • 阻止运行所有脚本文件,包括格式和配置文件(.ps1xml),模块脚本文件(.psm1)和PowerShell配置文件(.ps1)。
Undefined
  • 当前范围中未设置执行策略。
  • 如果所有作用域中的执行策略均为Undefined,则有效的执行策略为Restricted,这是默认的执行策略。
Unrestricted
  • 非Windows计算机的默认执行策略,不能更改。
  • 未签名的脚本可以运行。有运行恶意脚本的风险。
  • 在运行非本地Intranet区域中的脚本和配置文件之前警告用户。

更多问题可以参考powershell的官方文档

https://docs.microsoft.com/zh-cn/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7

laravel+react实战打造企业级高并发分布式电商小程序(一)

整体使用laravel7+react打造整个电商小程序。里面会涉及到高并发的知识,mysql的分库分表,主从读写分离的配置,redis集群的使用,缓存系统的使用,队列系统的使用等。

先初始化一个laravel的项目。然后配置好.env文件。

基础建设

我们使用前后端分离就要考虑跨域问题和安全问题。跨域使用cors解决,laravel7里面内置了cors的解决方案,我们只要修改config/cors.php配置文件就好了。

把里面的值更改一下。修改这个值的原因是因为我们会使用jwt传一个token的请求头过来进行验证。这个时候还是报跨域的错误,所以将supports_credentials值修改为true,如果不报错就不需要修改了。

1
2
3

'supports_credentials' => true,

把这个参数的值修改为true。

安全问题使用jwt的解决方案,安装jwt的包。

composer require lcobucci/jwt

在routes/api.php路由文件中增加下面的路由

1
2
3
4

//获取jwt token
Route::post('/require_token', 'JWT\RequireTokenController@requireToken');

在config下面新建jwt.php文件,里面内容如下

1
2
3
4
5
6
7
8

<?php

return [
'JWT_SECRET' => env('JWT_SECRET','DvYUz+woS7vVJe6ldY+PqWoUbhIyY9rShzM0NAfzxdU='),
'JWT_EXP_TIME' => env('JWT_EXP_TIME','36000'),
];

.env中增加下面的内容

1
2
3
4
5

# jwt
JWT_SECRET=DvYUz+woS7vVJe6ldY+PqWoUbhIyY9rShzM0NAfzxdU=
JWT_EXP_TIME=36000 //过期时间

app/http/middleware中创建中间件jwtCheck.php,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

<?php

namespace App\Http\Middleware;

use App\Models\Sys\ErrorModel;
use Closure;
use \Lcobucci\JWT\Parser;
use \Lcobucci\JWT\Signer\Hmac\Sha256;

class jwtCheck
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{

$parser = new Parser;
$signer = new Sha256;
$secret = config('jwt.JWT_SECRET');

if($request->hasHeader('Authorization')){
$token = $request->header('Authorization');
//解析token
$parse = $parser->parse($token);
//验证token合法性
if (!$parse->verify($signer, $secret)) {
return response()->json(['code'=>ErrorModel::JWT_ERROR, 'msg'=>'令牌错误!']);
}

//验证是否已经过期
if ($parse->isExpired()) {
return response()->json(['code'=>ErrorModel::JWT_ERROR, 'msg'=>'令牌过期!']);
}
}else{
return response()->json(['code'=>ErrorModel::JWT_ERROR, 'msg'=>'令牌缺失!']);
}
//把token放到参数里面
request()->offsetSet('token', $token);
return $next($request);
}
}

app/http下面的Kernel.php文件里面的$routeMiddleware变量里面增加下面内容,把中间件注册到系统中。

1
2
'jwtCheck' => \App\Http\Middleware\jwtCheck::class,

控制器

创建控制器

app/http/controller下面创建jwt文件夹,然后在jwt文件夹里面创建RequireTokenController.php文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

<?php

namespace App\Http\Controllers\JWT;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use \Lcobucci\JWT\Builder;
use \Lcobucci\JWT\Signer\Hmac\Sha256;
use Illuminate\Support\Facades\Redis;

class RequireTokenController extends Controller
{
public function requireToken(Builder $builder, Sha256 $signer) {

$secret = config('jwt.JWT_SECRET');
$time = time();
$expTime = config('jwt.JWT_EXP_TIME');

do {
//设置header和payload,以下的字段都可以自定义
$builder->setIssuer("cmp.wliot.com") //发布者
->setAudience("cmp.wliot.com") //接收者
->setId("abc", true) //对当前token设置的标识
->setIssuedAt($time) //token创建时间
->setExpiration($time + $expTime) //过期时间
// ->setNotBefore($time + 5) //当前时间在这个时间前,token不能使用
->set('uid', 30061); //自定义数据

//设置签名
$builder->sign($signer, $secret);
//获取加密后的token,转为字符串
$token = (string)$builder->getToken();
} while (Redis::exists($token));
//存入redis
// Redis::setex($token, $expTime, json_encode([]));

return $this->success($token);
}
}

在这里面使用到了$this->success()方法,这个方法来自controller类,我们需要编写这个方法。

app/http下面创建Utils文件夹,在里面创建Success.php文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<?php

namespace App\Http\Utils;

use App\Models\Sys\ErrorModel;

trait Success {

function success($data = []) {
$res = ['code'=>'0','msg'=>'请求成功!', 'data'=>$data];
return response()->json($res);
}
}

修改app/http/controllers/controller.php文件

1
2
3
4
5
6
7

use App\Http\Utils\Success; //引入刚才的文件

class Controller extends BaseController {
use AuthorizesRequests, DispatchesJobs, ValidatesRequests, Success; //在这里添加Success 也就是刚才的文件。
}

在这里面使用到了redis,所以我们需要启动你本地的redis服务器。启动之后就可以访问我们上面填写的路由了,使用postman访问你的路由。

Image text

可以看到返回了正确的token。

在后面的访问请求中我们需要使用这个token。我们把它加入请求头。在请求头新建一个Authorization的key,他的值就是我们的token。

laravel+react实战打造企业级高并发分布式电商小程序(二)

整体使用laravel7+react打造整个电商小程序。里面会涉及到高并发的知识,mysql的分库分表,主从读写分离的配置,redis集群的使用,缓存系统的使用,队列系统的使用等。

先初始化一个laravel的项目。然后配置好.env文件。

权限管理

既然是电商肯定有后台,要做权限管理这块。

先创建表,这里使用laravelmigration。下面是后台用户表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('auth_users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name', 100)->unique();
$table->string('email', 100)->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('auth_users');
}
}

下面是角色表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateRolesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('auth_roles', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name', 100)->unique();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('auth_roles');
}
}


下面是权限表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePermissionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('auth_permissions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name',100)->unique();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('auth_permissions');
}
}

下面是用户角色关联表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUserRolesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('auth_user_roles', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id');
$table->bigInteger('role_id');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('auth_user_roles');
}
}


下面是角色和权限关联表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateRolePerimissionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('auth_role_permissions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->default(0);
$table->bigInteger('role_id');
$table->bigInteger('perimission_id');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('auth_role_permissions');
}
}

下面这张是系统错误信息表,这里会返回错误信息,这些错误信息全部存放在这张表里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateSysErrorsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('sys_errors', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('code',100)->unique()->comment('错误编码');
$table->string('msg')->comment('错误信息');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('sys_errors');
}
}

创建完这些执行迁移命令就可以了。

php artisan migrate

当然了,在这之前确保你的.env文件已经配置好了数据库连接。

我们有了后台数据还需要一个管理员账户,使用seed填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

<?php

use App\Models\Auth\UserModel;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(UserModel $user)
{
$user->name = 'admin';
$user->email = 'admin@admin.com';
$user->password = Hash::make('123456');
$user->save();

}
}

运行填充命令

php artisan db:seed

控制器

这里我们使用一个laravel的扩展包,使用composer安装他

composer require thepatter/query-common

安装完后查看我的这个文章来使用这个包,根据文章里面做完之后再回来看下面的内容。

https://blog.csdn.net/Thepatterraining/article/details/105408363

接下来使用artisan命令创建我们的控制器。

php artisan make:queryController Auth/UserController

创建完之后在创建model

php artisan make:model Models/Auth/UserModel

修改刚才的Auth/UserModel,在里面增加table属性。

1
2
3
4
5
6
7
8
9
10
11
12
13

<?php

namespace App\Models\Auth;

use Illuminate\Database\Eloquent\Model;

class UserModel extends Model
{
//
protected $table = 'auth_users';
}

修改刚才的Auth/UserController控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

<?php

namespace App\Http\Controllers\Auth;

use QueryCommon\QueryController;
use App\Models\Auth\UserModel;
use App\Models\Auth\UserRoleModel;

class UserController extends QueryController
{
/**
* 字典数组
* ['表里的字段名' => '字典code',...]
*/
protected $dicArr = [];

/**
* 字段映射 可选,不填默认转成下划线格式
* ['搜索字段' => '表字段',...]
*/
protected $filedsAdapter = [];

/**
* 创建时候的字段映射 可选,不填默认转成下划线格式
* ['输入字段' => '表字段']
*/
protected $createAdapter = [
'name' => 'name',
'email' => 'email',
];

//定义表名 格式: table as t
protected $shortTableName;


protected function getModel() {
$this->model = new UserModel;
return $this->model;
}

}


这时候我们对用户的增删改查操作就完成了,是不是超级简单呢。这主要依赖于我们的query-common扩展包。

我们接下来只需要添加对应的路由就可以了。在routes下面创建Api/Auth文件夹。在里面创建index.php路由文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

<?php
use Illuminate\Support\Facades\Route;


Route::prefix('auth')->namespace('Auth')->group(function () {
//后台创建用户
Route::post('user', 'UserController@createInfo');

//后台查询管理员列表
Route::get('users', 'UserController@queryList');

//后台更新管理员信息
Route::put('user/{id}', 'UserController@updateInfo');

//后台删除管理员
Route::delete('user/{id}', 'UserController@deleteInfo');

});

添加完路由文件后我们需要自动加载路由文件,请看我的这篇文章,路由自动加载。

https://blog.csdn.net/Thepatterraining/article/details/105386868

接下来可以使用postman来测试了。

获取用户列表

Image text

其他的就不放图了。

角色

有了用户就有角色了,我们的角色表在之前已经创建好了,我们现在同样的方法创建controller和model。

执行下面的命令。-m参数是model,这里指定model后,如果model不存在会自动创建。

php artisan make:queryController Auth/RoleController -m Models/Auth/RoleModel

修改刚才的Auth/RoleModel,在里面增加table属性。

1
2
3
4
5
6
7
8
9
10
11
12
13

<?php

namespace App\Models\Auth;

use Illuminate\Database\Eloquent\Model;

class RoleModel extends Model
{
//
protected $table = 'auth_roles';
}

app/Http/Controllers/Auth/RoleController文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

<?php

namespace App\Http\Controllers\Auth;

use QueryCommon\QueryController;
use App\Models\Auth\RoleModel;

class RoleController extends QueryController
{
/**
* 字典数组
* ['表里的字段名' => '字典code',...]
*/
protected $dicArr = [];

/**
* 字段映射 可选,不填默认转成下划线格式
* ['搜索字段' => '表字段',...]
*/
protected $filedsAdapter = [];

/**
* 创建时候的字段映射 可选,不填默认转成下划线格式
* ['输入字段' => '表字段']
*/
protected $createAdapter = [];

//定义表名 格式: table as t
protected $shortTableName;


protected function getModel() {
$this->model = new RoleModel();
return $this->model;
}

}

接着在刚才的routes/Api/Auth/index路由文件中增加下面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13

//后台查询角色列表
Route::get('roles', 'RoleController@queryList');

//创建角色
Route::post('role', 'RoleController@createInfo');

//更新角色
Route::put('role/{id}', 'RoleController@updateInfo');

//删除角色
Route::delete('role/{id}', 'RoleController@deleteInfo');

增加后是下图这样

Image text

同样使用postman测试。

Image text

用户和角色关联

有了用户和角色就要把这两个关联起来了,我们通过在创建用户和修改用户的时候关联角色,在这时候就要更改之前的Auth/UserController了。

增加下面的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

/**
* 创建完用户后执行的操作
*/
protected function createAfter($id) {
$this->userRole($id);
}

//更新完主表之后可以进行的操作
protected function updateAfter($id) {
//更新完用户信息需要更新用户角色关联
//先删除所有关联,再重新创建
UserRoleModel::where('user_id', $id)->delete();

$this->userRole($id);

}


/**
* 用户角色操作,先删除用户的所有角色,再创建角色关联
* @param int $id 用户id
*/
private function userRole($id) {
$ids = $this->request->input('roleIds');
//组织数据
$insertDatas = [];
foreach ($ids as $roleId) {
$insertData = [];
$insertData['user_id'] = $id;
$insertData['role_id'] = $roleId;
$insertData['created_at'] = date('Y-m-d H:i:s');
$insertDatas[] = $insertData;
}
//一次性插入
UserRoleModel::insert($insertDatas);
}

createAfter方法是在创建完用户后执行的,updateAfter方法是更新完用户信息后执行的。传入用户id。我们在这里删除之前的用户和角色关联,把新的循环添加到数组中,一次性插入数据库。因为循环插入会进行多次数据库io操作,而数据库io是比较耗费资源和时间的,所以我们尽可能少进行数据库操作。

在创建和更新时候我们还需要验证参数,我们再添加下面的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

/**
* 在创建之前调用,用来验证参数
*/
protected function createBefore() {
//检测页码和每页数量
$rules = [
'name' => 'required|unique:auth_users,name',
'email' => 'required|email|unique:auth_users,email',
'password' => 'required',
'roleIds' => 'required|array',
];
$messages = [
'name.required' => '用户名为必填项',
'name.unique' => '用户已经存在',
'email.required' => '邮箱为必填项',
'email.unique' => '邮箱已经存在',
'email.email' => '请输入正确的邮箱格式',
'password.required' => '密码为必填项',
'roleIds.required' => '角色为必填项',
'roleIds.array' => '角色必须是数组类型',
];
$this->valid($rules, $messages);

//判断角色数组是不是在数据库都存在
$roleNum = RoleModel::whereIn('id', $this->request->roleIds)->count();
if ($roleNum != count($this->request->roleIds)) {
//角色id不对
throw new CommonException(ErrorModel::ROLE_NOT_FOUND);
}
}

/**
* 在创建之前调用,用来验证参数
*/
protected function updateBefore() {
//检测页码和每页数量
$rules = [
'name' => 'required',
'email' => 'required|email',
'roleIds' => 'required|array',
];
$messages = [
'name.required' => '用户名为必填项',
'email.required' => '邮箱为必填项',
'email.email' => '请输入正确的邮箱格式',
'roleIds.required' => '角色为必填项',
'roleIds.array' => '角色必须是数组类型',
];
$this->valid($rules, $messages);

//判断角色数组是不是在数据库都存在
$roleNum = RoleModel::whereIn('id', $this->request->roleIds)->count();
if ($roleNum != count($this->request->roleIds)) {
//角色id不对
throw new CommonException(ErrorModel::ROLE_NOT_FOUND);
}
}

createBefore方法在创建用户之前调用,updateBefore方法在更新之前调用,用来验证参数信息。验证角色在数据库中不存在后要返回错误,直接throw抛出异常即可,因为在外部已经用try catch捕获了,这里返回的是一个error code,在Sys/ErrorModel中定义一个错误信息。为了方便管理,所有的错误以常量的方式定义在errorModel里面。真正的错误信息在我们一开始创建的sys_error数据表中。我们在数据表中添加一条错误信息,code是100002的,msg是角色未定义!

1
2
3

const ROLE_NOT_FOUND = '100002'; //角色未定义!

我们可以传一些错误的角色id给到接口中,使用postman来测试一下。因为这个用户已经创建了,所以返回了用户已经存在的错误信息。

Image text

权限

我们使用同样的方式创建权限的controllermodel

php artisan make:queryController Auth/PermissionController -m Models/Auth/PermissionModel

和上面一样,修改刚才的Auth/PermissionModel,在里面增加table属性。后面的就不再说这里了。

1
2
3
4
5
6
7
8
9
10
11
12
13

<?php

namespace App\Models\Auth;

use Illuminate\Database\Eloquent\Model;

class PermissionModel extends Model
{
//
protected $table = 'auth_permissions';
}

增加路由。

1
2
3
4
5
6
7
8
9
10
11
12
13

//后台查询权限列表
Route::get('permissions', 'PermissionController@queryList');

//创建权限
Route::post('permission', 'PermissionController@createInfo');

//更新权限
Route::put('permission/{id}', 'PermissionController@updateInfo');

//删除权限
Route::delete('permission/{id}', 'PermissionController@deleteInfo');

使用postman测试。

Image text

现在呢,我们权限管理的后端接口就算做完了,当然了,后面还会根据需要调整。

laravel orm

laravel 使用 orm 调用数据库查询的时候很方便,我们只需要配置完数据库连接后创建model。

比如我们查询用户,我们首先要有一个user model

可以使用 artisan创建

php artisan make:model userModel

如果我们需要查询一个用户信息,只需要这样

1
2
userModel::where('id', 1)->first();
(new userModel)->where('id', 1)->first();

上面的两种方法都可以实现查询用户id为1的用户信息。那他们有什么区别呢?第一种方法更为优雅。

那这个是怎么实现的呢,一般我们要么定义一个静态方法用来静态调用,要么定义一个对象方法需要使用对象调用。

其实很简单,他只是用到了php魔术方法

来看一下下面两个魔术方法:

  • __call()
  • __callStatic()

看一下php文档中的介绍

__call()

在对象中调用一个不可访问方法时,__call() 会被调用。

public __call ( string $name , array $arguments ) : mixed

__callStatic()

在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。

public static __callStatic ( string $name , array $arguments ) : mixed

$name 参数是要调用的方法名称。$arguments 参数是一个枚举数组,包含着要传递给方法 $name 的参数。

我们要让一个对象方法可以静态调用就要通过__callStatic()魔术方法了。

1
2
3
4
5

public static function __callStatic( string $name , array $arguments) {
return (new static)->$name(...$arguments);
}

这样的话当我们调用userModel::where()的时候实际上它内部会创建一个对象然后再对象调用where

但是这样会产生一个问题,如果当我们调用的方法不存在的时候怎么办呢。

可以通过try catch来捕获错误进行错误处理。laravel内部就是这么实现的。在laravel的model文件下,是这么运用__call()的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

/**
* Handle dynamic method calls into the model.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (in_array($method, ['increment', 'decrement'])) {
return $this->$method(...$parameters);
}

return $this->forwardCallTo($this->newQuery(), $method, $parameters);
}

他里面调用了forwardCallTo方法,我们来看一下这个方法。

这个方法存在ForwardsCalls这个trait中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

/**
* Forward a method call to the given object.
*
* @param mixed $object
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
protected function forwardCallTo($object, $method, $parameters)
{
try {
return $object->{$method}(...$parameters);
} catch (Error | BadMethodCallException $e) {
$pattern = '~^Call to undefined method (?P<class>[^:]+)::(?P<method>[^\(]+)\(\)$~';

if (!preg_match($pattern, $e->getMessage(), $matches)) {
throw $e;
}

if ($matches['class'] != get_class($object) ||
$matches['method'] != $method) {
throw $e;
}

static::throwBadMethodCallException($method);
}
}

他在调用的时候通过try catch来捕获错误。

还有另外一种方法同样可以判断这个类有没有这个方法,那就是先通过get_class_methods($className)这个函数获取到类的所有方法,然后我们就可以自己判断了。

1
2
3
4
5
6
7
8
9
10

public function __call($method, $parameters)
{
$classFuns = get_class_methods($this);
if (!in_array($method, $classFuns)) {
return "没有这个方法"; //返回错误
}

return $this->$method(...$parameters);
}

这样也不失为一种方法啊,大家还有其他好方法的话欢迎交流。

laravel神器教你一秒搞定增删改查业务模块

还在为了不断写增删改查而烦恼不堪嘛?还在为了重复写代码而头疼嘛?这个laravel神器拯救你的大脑,解放你的双手。让你有更多的时间去写出更好的代码。

安装

首先使用composer安装

composer require thepatter/query-common

安装之后创建一个command

php artisan make:command MakeQueryCommand

阅读全文 »

laravel路由自动加载

laravel 自带的路由文件有四个

  • api.php 文件存放 api 路由,会自动加载api前缀和一些中间件。
  • channels.php 文件用于注册应用支持的所有事件广播频道。
  • console.php 文件用于定义所有基于闭包的控制台命令,每个闭包都被绑定到一个控制台命令并且允许与命令行 IO 方法进行交互,尽管这个文件并不定义 HTTP 路由,但是它定义了基于控制台的应用入口(路由)。
  • web.php 如果应用无需提供无状态的、RESTful 风格的 API,那么路由基本上都要定义在 web.php 文件中。会自动加载web中间件。

我们常用的无非是api和web路由,一开始我们可以都写在里面,那当程序不断扩大,路由达到几千个,几万个甚至更多,放在一个文件里显示难以维护,难以查找。

这时候我们需要把路由分到不同的路由文件中去,我们在routes目录下创建api文件夹,来存放相关的api路由。

阅读全文 »

基于docker快速搭建多平台laravel环境-laradock

现在docker技术越来越火,docker的应用也越来越多。

我们为什么要用docker呢,因为它能提供你一个纯净的环境,能统一所有开发人员的环境,公司的技术有很多人,那每个人装的环境都可能不一样,你是php7.3,他是php7.0,你是mysql8.0,他是mysql5.6,这些环境上的差异有时候会导致代码的错误。

还有环境这东西装一次就够用了,你家里的电脑环境和公司的环境也有可能不一致。使用docker装环境之后,我们可以装完之后打包起来,在任何一个docker上运行这个配置文件都可以生成相同的环境。

下载安装

git clone https://github.com/Laradock/laradock.git

阅读全文 »