本章我们首先从ThinkPHP5.0
的数据库访问层架构设计原理开始,然后熟悉下数据库的配置,并掌握如何进行基础的查询操作,并简单介绍了分布式、存储过程及事务,学习内容主要包括:
数据库架构设计
使用框架开发应用,一般不需要直接操作数据库,而是通过框架封装好的数据库中间层对数据库进行操作。这样的好处主要有两个:一是简化数据库操作,二是做到跨数据库的一致性。这种设计的中间层通常称之为数据库访问抽象层,简称数据访问层(DAL
),ThinkPHP5的数据访问层是基于PHP内置的PDO
对象实现。一般抽象层本身并不直接操作数据库,而是通过驱动来实现具体的数据库操作。
ThinkPHP5.0
的数据库设计相比之前版本更加合理,数据访问层划分的更细化,把数据访问对象分成了连接器、查询器、生成器等多个对象,并通过数据库访问入口类统一调用,分工更明确,各司其职,欲知详情且听我慢慢道来。
ThinkPHP数据访问层设计示意图:
5.1版本的架构略微进行了一些调整,变成:
数据库入口类Db
平常我们的数据库操作使用的类库一般都是数据库的入口类think\Db
。这个类非常的简单,主要就是一个connect
方法,根据数据库配置参数连接数据库(注意这里的连接并非真正的连接数据库,只是做好了随时连接的准备工作,只有在实际查询的时候才会真正去连接数据库,是一种惰性连接)并获取到数据库连接对象的实例。
Db
类都是静态方法调用,但看起来这个类啥都没实现,那是怎么操作数据库的呢,其实就是封装了数据库操作方法的静态调用(利用__callStatic
方法),下面是代码实现:
// 调用驱动类的方法
public static function __callStatic($method, $params)
{
// 自动初始化数据库
return call_user_func_array([self::connect(), $method], $params);
}
理论上来说,框架并不依赖Db
类,该类的存在只是为了简化数据库抽象层的操作而提供的一个工厂类,否则你就需要单独实例化不同的数据库连接类。因此,看似可有可无的Db
类就成了数据访问层实现的点睛之笔了。
所有的数据库操作都是经过
Db
类调用,并且Db
类是一个静态类,但Db
类自身只有一个公共方法connect
。
连接器类Connection
顾名思义,连接类的作用就是连接数据库,也称为连接器。我们知道,不同的数据库的连接方式和参数都是不同的,连接类就是要解决这个差异问题。
数据库入口类里面实例化的类其实就是对应数据库的连接类,连接类的基类是think\db\Connection
。例如,需要连接Mysql
数据库的话,就必须定义一个Mysql
连接类(内置由think\db\connector\Mysql
类实现,继承了think\db\Connection
类),当然具体的连接类名没有固定的规范(例如,MongoDb
的连接类就是think\mongo\Connection
)。如果某个数据库的连接扩展类没有继承think\db\Connection
,那就意味着所有的数据库底层操作有可能被接管,在个别特殊的数据库的扩展中就有类似的实现,例如MongoDb
数据库扩展。
数据库连接都是惰性的,只有最终执行SQL的时候才会进行连接。
连接器是数据访问层的基础,基于PHP本身的PDO
实现(如果你还不了解PDO
,请参考PHP官方手册中PDO部分,不在本书的讨论范畴),连接类的主要作用就是连接具体的数据库,以及完成基本的数据库底层操作,包括对分布式、存储过程和事务的完善处理。而更多的数据操作则交由查询类完成。
框架内置的连接类包括:
数据库 | 连接类 |
---|---|
Mysql | think\db\connector\Mysql |
Pgsql | think\db\connector\Pgsql |
Sqlite | think\db\connector\Sqlite |
Sqlsrv | think\db\connector\Sqlsrv |
如果是仅仅使用原生SQL查询的话,只需要使用连接类就可以了(通过调用Db类完成)
连接器类的作用小结:
- 连接数据库;
- 获取数据表和字段信息;
- 基础查询(原生查询);
- 事务支持;
- 分布式支持;
查询器类Query
除了基础的原生查询可以在连接类完成之外,其它的查询操作都是调用查询类的方法,查询类内完成了数据访问层最重要的工作,衔接了连接类和生成类,统一了数据库的查询用法,所以查询类是不需要单独驱动配合的,我们也称之为查询器。无论采用什么数据库,我们的查询方式是统一的,因为数据访问层核心只有一个唯一的查询类:think\db\Query
。
Query
类封装了所有的数据库CURD
方法的优雅实现,包括链式方法及各种查询,并自动使用了PDO
参数绑定(参数自动绑定是在生成器类解析生成SQL时完成),最大程度地保护你的程序避免受数据库注入攻击,查询操作会调用生成类生成对应数据库的SQL语句,然后再调用连接类提供的底层原生查询方法执行最终的数据库查询操作。
所有的数据库查询都使用了
PDO
的预处理和参数绑定机制。你所看到的大部分数据库方法都来自于查询类而并非Db
类,这一点很关键,也就是说虽然我们始终使用Db
类操作数据库,而实际上大部分方法都是由查询器类提供的方法。
生成器类Builder
生成类的作用是接收Query
类的所有查询参数,并负责解析生成对应数据库的原生SQL
语法,然后返回给Query
类进行后续的处理(包括交给连接类进行SQL
执行和返回结果处理),也称为(语法)生成器。生成类的作用其实就是解决不同的数据库查询语法之间的差异。查询类实现了统一的查询接口,而生成类负责数据库底层的查询对接。
生成类一般不需要自己调用,而是由查询类自动调用的。也可以这么理解,生成类和查询类是一体的,事实上它们合起来就是通常我们所说的查询构造器(因为实际的查询操作还是在连接器中执行的)。
通常每一个数据库连接类都会对应一个生成类,框架内置的生成类包括:
数据库 | 生成类 |
---|---|
Mysql | think\db\builder\Mysql |
Pgsql | think\db\builder\Pgsql |
Sqlite | think\db\builder\Sqlite |
Sqlsrv | think\db\builder\Sqlsrv |
这些生成类都继承了核心提供的生成器基类think\db\Builder
,每个生成器类只需要提供差异部分的实现。
数据库配置
数据库的配置参数有很大的学问,也是你掌握数据库操作的基础,主要用于数据库的连接以及查询的相关设置。
数据库的配置参数用于连接类的架构方法,而由于我们并不直接操作连接类,所以,配置参数主要通过Db
类传入并设置到当前的数据库连接类。
数据库配置分为静态配置和动态配置两种方式,静态配置是指在数据库配置文件中进行配置,动态配置是指在Db类或者Query
类的connect
方法中传入动态的配置参数。
安装好ThinkPHP5之后,默认在application
目录下面会有一个database.php
文件,这就是应用的数据库配置文件,如果你的模块需要单独的数据库配置文件,那么只需要在模块目录下面创建一个database.php
文件即可,并且只需要定义和应用数据库配置文件有差异的部分。
数据库配置文件中配置的是默认的数据库连接配置,如果你有多个数据库连接,额外的数据库连接是在应用配置文件中完成的(参考后面的动态数据库连接)。
├─application
│ ├─index
│ │ ├─database.php (模块)数据库配置文件
│ │ └─ ...
│ ├─database.php (应用)数据库配置文件
│ └─ ...
我们下面的数据库配置文件都以应用数据库配置文件为例说明。
默认的应用数据库配置文件如下:
return [
// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '127.0.0.1',
// 数据库名
'database' => '',
// 用户名
'username' => 'root',
// 密码
'password' => '',
// 端口
'hostport' => '',
// 连接dsn
'dsn' => '',
// 数据库连接参数
'params' => [],
// 数据库编码默认采用utf8
'charset' => 'utf8',
// 数据库表前缀
'prefix' => '',
// 数据库调试模式
'debug' => true,
// 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'deploy' => 0,
// 数据库读写是否分离 主从式有效
'rw_separate' => false,
// 读写分离后 主服务器数量
'master_num' => 1,
// 指定从服务器序号
'slave_no' => '',
// 是否严格检查字段是否存在
'fields_strict' => true,
// 数据集返回类型
'resultset_type' => 'array',
// 自动写入时间戳字段
'auto_timestamp' => false,
// 时间字段取出后的默认时间格式
'datetime_format' => 'Y-m-d H:i:s',
// 是否需要进行SQL性能分析
'sql_explain' => false,
// Builder类
'builder' => '',
// Query类
'query' => '\\think\\db\\Query',
];
最关键的参数就是下面几个(其它参数后面会陆续涉及):
参数名 | 作用 |
---|---|
type | 数据库类型或者连接类名 |
hostname | 数据库服务器地址(一般是IP地址,默认为127.0.0.1 ) |
username | 数据库用户名(默认为root ) |
password | 数据库用户密码(默认为空) |
database | 使用的数据库名称 |
charset | 数据库编码(默认为utf8 ) |
type
参数严格来说其实配置的是连接类名(而不是数据库类型),支持命名空间完整定义,不带命名空间定义的话,默认采用\think\db\connector
作为命名空间(内置连接类的命名空间)。你完全可以在应用中扩展自己的数据库连接类,例如配置为:
// 配置数据库类型(连接类)为自定义
'type' => '\app\db\Mysql',
这样就可以自己替换或者扩展一些额外的数据库操作方法。
自定义连接类的时候,请注意设置数据库配置中的
builder
参数避免找不到对应生成器类。
ThinkPHP5.0
采用PDO来统一操作数据库,而连接类的最关键的作用就是通过配置连接到数据库,PDO的连接方法参数如下:
PDO::__construct ( 'DSN' ,'用户名','密码','连接参数(数组)' )
数据库的数据源名称(DSN
)是最关键的一个参数,连接类负责把数据库配置参数自动转换为一个有效的DSN
数据源名称。如果你有特殊的连接语法需求,则可以通过配置数据库配置文件中的dsn
参数来解决,该配置参数的值会直接用于PDO连接,例如:
// 连接dsn
'dsn' => 'mysql:unix_socket=/tmp/mysql.sock;dbname=demo',
数据库支持断线重连机制(默认关闭),可以设置(V5.0.6+
版本仅支持Mysql数据库,V5.0.9+
版本开始支持内置所有数据库):
// 开启断线重连
'break_reconnect' => true,
除了DSN
数据源名称,PDO
的连接参数也可以单独设置,每个连接驱动都有自己的连接参数设置,Mysql
连接器内置采用的参数包括如下:
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
可以在数据库配置文件中设置params
参数,会和内置的连接参数合并,例如:
// 数据库连接参数
'params' => [
// 使用长连接
\PDO::ATTR_PERSISTENT => true,
// 数据表字段统一转换为小写
\PDO::ATTR_CASE => \PDO::CASE_LOWER,
],
常用数据库连接参数(params)可以参考PHP在线手册中的以PDO::ATTR_
开头的常量。
如何开始查询
在开始学习查询之前,我们首先在demo
数据库中创建一个data
测试表。
CREATE TABLE IF NOT EXISTS `data`(
`id` int(8) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT '名称',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
然后设置数据库配置文件内容为(如果有密码请自行修改):
return [
// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '127.0.0.1',
// 数据库名
'database' => 'demo',
// 用户名
'username' => 'root',
// 密码
'password' => '',
// 开启数据库调试
'debug' => true,
];
特别注意我们在配置中开启了
debug
参数,表示开启数据库的调试模型,开启后会记录数据库的连接信息和SQL日志,数据库的调试模式和应用的调试模式是两个不同的概念。
配置完数据库连接信息后,我们就可以直接使用Db
类进行数据库运行原生SQL操作了,你无需关心数据库的连接操作,系统会自动使用数据库配置参数进行数据库的连接操作。
Db
类的方法都是静态调用(不需要去实例化think\Db
类),Db
类的查询方法有很多(大部分查询都是使用的查询构造器),本章内容暂时只讲两个用于原生查询的方法,包括query
(查询操作)和execute
(写入操作),更多的查询方法会在查询构造器章节作出详细讲解。
数据库查询的所有示例都需要写到一个控制器的方法里面,我们现在假设你已经定义了一个下面的控制器操作方法:
<?php
namespace app\index\controller;
use think\Db;
class Index
{
public function index()
{
// 这里是数据库操作的测试代码
// ...
return;
}
}
一般来说并不建议在控制器的操作方法中直接操作数据库Db类,但由于我们还没涉及到模型章节的内容,因此,目前的写法仅为了演示数据库的示例代码。
并且在应用配置文件中开启页面Trace显示:
// 应用Trace
'app_trace' => true,
开启页面Trace的作用是为了方便我们查看当前请求的SQL语句信息以及执行时间(需要开启数据库调试模式后有效)。
然后在index
操作方法中添加下面测试代码:
Db::execute('insert into data (id, name) values (1, "hinkphp")');
Db::query('select * from data where id=1');
访问页面后,显示空白,点击右下角的对数据表的CURD操作,除了
select
和存储过程调用使用query
方法之外,其它的操作都使用execute
方法,这里就不再一一演示了。
就可以打开页面Trace信息,切换到SQL一栏,可以看到下面的类似信息
第一条表示数据库的连接信息(连接消耗时间以及连接的DSN),后面的两条就表示当前操作执行的SQL语句,由于我们使用的是原生查询,所以SQL语句和你的代码里面的SQL语句是一致的,每条SQL语句最后会显示该SQL语句的执行消耗时间。
细心的朋友会发现Db
类里面并没有query
和execute
方法,其实在调用Db
类的方法(connect
方法除外)之前,都会先调用connect
方法进行数据库的初始化(前面提过的__callStatic
方法),由于connect
方法会返回一个数据库连接类的对象实例(根据配置参数实现了单例),所以Db
类调用的query
和execute
方法其实就是连接器类(Connection
)的方法,这一点必须理解,否则你很难理解数据库的查询操作。
使用参数绑定
上面的例子是实际开发中其实并不建议,原则上我们在使用原生查询的时候最好使用参数绑定避免SQL注入,例如:
Db::execute('insert into data (id, name) values (?, ?)',[2,'kancloud']);
Db::query('select * from data where id=?',[2]);
页面Trace信息中会显示实际运行的SQL语句
也支持命名占位符绑定,例如:
Db::execute('insert into data (id, name) values (:id, :name)',['id'=>3,'name'=>'topthink']);
Db::query('select * from data where id=:id',['id'=>3]);
参数绑定的变量不需要使用引号
同样显示的实际执行SQL如下:
我们看到查询语句中的id的值是字符串的,由于参数绑定默认都是使用的字符串,如果需要指定为数字类型,可以使用下面的方式:
Db::execute('insert into data (id, name) values (:id, :name)',['id'=>[4,\PDO::PARAM_INT],'name'=>'onethink']);
Db::query('select * from data where id=:id',['id'=>[4,\PDO::PARAM_INT]]);
这次查看实际的执行SQL会有细微的变化
PDO命名占位绑定不支持一个参数多处绑定,下面的用法会报错:
Db::execute('insert into data (name) values (:name),(:name)',['name'=>'thinkphp']);
该错误信息表示你的参数绑定参数数量不符。
查询返回值
使用Db
类查询数据库的话,query
方法的返回值是一个二维数组的数据集,每个元素就是一条记录,例如:
array (size=1)
0 =>
array (size=5)
'id' => int 8
'name' => string 'thinkphp' (length=8)
如果没有查询到任何数据,返回值就是一个空数组。
相比query
方法,execute
方法的返回值就比较单纯,一般就是返回影响(包括新增和更新)的记录数,如果没有影响任何记录,则返回值为0
,所以千万不要用布尔值来判断execute
是否执行成功,事实上,在5.0
里面不需要判断是否成功,因为如果发生错误一定会抛出异常。
动态连接数据库
当你需要使用多个数据库连接的时候,就需要使用connect
方法动态切换到另外一个数据库连接,假设存在另外一个数据库test
,并且复制data
过去更名为test
,然后测试下面的示例:
Db::query('select * from data where id = 2');
Db::connect([
// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '127.0.0.1',
// 数据库名
'database' => 'test',
// 用户名
'username' => 'root',
// 密码
'password' => '',
// 开启调试模式
'debug' => true,
])->query('select * from test where id = 1');
Db::query('select * from data where id = 3');
页面Trace的显示信息可以看出来使用了两次数据库连接和执行了三次查询,并且数据库连接切换并没有影响默认的查询(第三个查询还是使用的默认数据库配置连接,test
数据库中并不存在data
表,如果连接的还是第二个数据库连接的话肯定会报错)。
有时候,我们只需要设置一些基本的数据库配置参数,可以简化成一个字符串格式定义(该格式为ThinkPHP使用规范,而不是PDO连接规范,不要和DSN
混淆起来):
Db::connect('mysql://root@127.0.0.1/demo#utf8')
->query('select * from data where id = 1');
字符串格式的连接信息规范格式如下:
数据库类型://用户名[:用户密码]@数据库服务器地址[:端口]/数据库名[?参数1=值&参数2=值]#数据库编码
Db
类的connect
方法会返回一个数据库连接对象实例,相同的连接参数返回的是同一个对象实例,除非你强制重新实例化,例如:
Db::connect([
// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '127.0.0.1',
// 数据库名
'database' => 'demo',
// 用户名
'username' => 'root',
// 密码
'password' => '',
],true)->query('select * from data where id = 1');
这样,每次调用都会重新实例化数据库的连接类。
为了便于统一管理,你可以把数据库配置参数纳入配置文件,例如在应用配置文件中添加:
'db_config' => [
// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '127.0.0.1',
// 数据库名
'database' => 'demo',
// 用户名
'username' => 'root',
// 密码
'password' => '',
],
或者使用字符串方式定义
'db_config' => 'mysql://root@127.0.0.1/demo',
上面的
db_config
配置参数不是在数据库配置文件中定义,而是在应用配置文件或者模块配置文件中定义。
然后,使用下面的方式来动态连接获取切换连接
Db::connect('db_config')
->query('select * from data where id=:id', ['id'=>3]);
当connect
方法传入的连接参数是字符串并且不包含/
等特殊符号的话,表示使用的是预定义数据库配置参数。
分布式支持
数据访问层支持分布式数据库,包括读写分离,要启用分布式数据库,需要开启数据库配置文件中的deploy
参数:
return [
// 启用分布式数据库
'deploy' => 1,
// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '192.168.1.1,192.168.1.2',
// 数据库名
'database' => 'demo',
// 数据库用户名
'username' => 'root',
// 数据库密码
'password' => '',
// 数据库连接端口
'hostport' => '',
];
启用分布式数据库后,hostname
参数是关键,hostname
的个数决定了分布式数据库的数量,默认情况下第一个地址就是主服务器。
主从服务器支持设置不同的连接参数,包括:
连接参数 |
---|
username |
password |
hostport |
database |
dsn |
charset |
如果主从服务器的上述参数一致的话,只需要设置一个,对于不同的参数,可以分别设置,例如:
return [
// 启用分布式数据库
'deploy' => 1,
// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '192.168.1.1,192.168.1.2,192.168.1.3',
// 数据库名
'database' => 'demo',
// 数据库用户名
'username' => 'root,slave,slave',
// 数据库密码
'password' => '123456',
// 数据库连接端口
'hostport' => '',
// 数据库字符集
'charset' => 'utf8',
];
记住,要么相同,要么每个都要设置。
还可以设置分布式数据库的读写是否分离,默认的情况下读写不分离,也就是每台服务器都可以进行读写操作,对于主从式数据库而言,需要设置读写分离,通过下面的设置就可以:
'rw_separate' => true,
在读写分离的情况下,默认第一个数据库配置是主服务器的配置信息,负责写入数据,如果设置了master_num
参数,则可以支持多个主服务器写入(每次随机连接其中一个主服务器)。其它的地址都是从数据库,负责读取数据,数量不限制。每次连接从服务器并且进行读取操作的时候,系统会随机进行在从服务器中选择。同一个数据库连接的每次请求只会连接一次主服务器和从服务器,如果某次请求的从服务器连接不上,会自动切换到主服务器进行查询操作。
如果不希望随机读取,或者某种情况下其它从服务器暂时不可用,还可以设置slave_no
指定固定服务器进行读操作,slave_no
指定的序号表示hostname
中数据库地址的序号,从0
开始。
调用查询类或者模型的CURD
操作的话,系统会自动判断当前执行的方法是读操作还是写操作并自动连接主从服务器,如果你用的是原生SQL,那么需要注意系统的默认规则: 写操作必须用数据库的execute
方法,读操作必须用数据库的query
方法,否则会发生主从读写错乱的情况。
发生下列情况的话,会自动连接主服务器:
- 使用了数据库的写操作方法(
execute
/insert
/update
/delete
以及衍生方法); - 如果调用了数据库事务方法的话,会自动连接主服务器;
- 从服务器连接失败,会自动连接主服务器;
- 调用了查询构造器的
lock
方法; - 调用了查询构造器的
master
方法
主从数据库的数据同步工作不在框架实现,需要数据库考虑自身的同步或者复制机制。如果在大数据量或者特殊的情况下写入数据后可能会存在同步延迟的情况,可以调用
master()
方法进行主库查询操作。
在实际生产环境中,很多云主机的数据库分布式实现机制和本地开发会有所区别,但通常会采下面用两种方式:
- 第一种:提供了写IP和读IP(一般是虚拟IP),进行数据库的读写分离操作;
- 第二种:始终保持同一个IP连接数据库,内部会进行读写分离IP调度(阿里云就是采用该方式)。
存储过程调用
数据访问层支持存储过程调用,调用数据库存储过程使用下面的方法:
$resultSet = Db::query('call procedure_name');
foreach ($resultSet as $result) {
}
存储过程返回的是一个数据集,如果你的存储过程不需要返回任何的数据,那么也可以使用execute
方法:
Db::execute('call procedure_name');
存储过程可以支持输入和输出参数,以及进行参数绑定操作。
$resultSet = Db::query('call procedure_name(:in_param1,:in_param2,:out_param)', [
'in_param1' => $param1,
'in_param2' => [$param2, PDO::PARAM_INT],
'out_param' => [$outParam, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 4000],
]);
输出参数的绑定必须额外使用PDO::PARAM_INPUT_OUTPUT
,并且可以和输入参数公用一个参数。
无论存储过程内部做了什么操作,每次存储过程调用仅仅被当成一次查询。
数据库事务
5.0对数据库事务的封装更为完善,事务的支持由连接器类来完成,但查询器类中也对事务进行了封装调用,不过我们仍然只需要通过Db类便可完成事务操作。
使用事务处理的话,需要数据库引擎支持事务处理。比如
MySQL
的MyISAM
类型不支持事务处理,需要使用InnoDB
引擎。
最简单的方法是使用transaction
方法操作数据库事务,会自动控制事务处理,当发生任何异常会自动回滚,例如:
Db::transaction(function () {
Db::table('user')->find(1);
Db::table('user')->where('id', 1)->save(['name' => 'thinkphp']);
Db::table('user')->delete(1);
});
也可以手动控制事务,例如:
// 启动事务
Db::startTrans();
try{
Db::table('user')->find(1);
Db::table('user')->where('id',1)->save(['name'=>'thinkphp']);
Db::table('user')->delete(1);
// 提交事务
Db::commit();
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
}
在事务操作的时候,确保你的数据库连接是同一个,否则事务会失效,
V5.0.9
版本之前的db
助手函数都是默认重新链接数据库的,请不要在事务中使用。
总结
通过本章的学习,你应该了解了5.0的数据库架构设计和数据库抽象访问层的组成,以及如何配置数据库信息和使用基础的原生查询,掌握了用Db
类的connect
方法切换不同的数据库连接,基本了解了存储过程及事务的用法。后面一章,我们会先来了解下数据库的创建和数据迁移,之后就会进入真正的数据库查询的学习了。
下一篇:第二章:数据创建和迁移