引言
上一讲 后端如何理解数据库的数据,说到我们需要一个工具,它能够把 ResultSet 中的信息自动映射到对象上,也能够把对象中的信息映射到 SQL 语句中的参数上。
这样的框架在java开发领域有 hibernate 和 mybatis。使用mybatis还是不可以完全依照面向对象的思想去处理数据访问,但是在 sql 构建层面给了我们足够的灵活性,所以这里还是讲 mybatis。那么 mybatis 到底是什么呢?
以下定义引自 官方文档:
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
实际上 mybatis 做了两个核心的工作:
- 按照JDBC规范建立和数据库的连接并进行管理
- 利用反射处理java对象和数据库数据之间的转换
接下来,会有三四篇将关于mybatis的文章,作为入门熟练的引导教程,会涉及:
- mybatis对于jdbc来说做了什么
- spring、spring-boot 对于 mybatis来说做了什么
- mybaits入门的核心概念解析
- mybatis使用案例
- mybatis使用技巧
本篇只涉及原生的 mybatis ,和spring等无关,也和最终实际项目的构建无关,实际项目直接结合spring-boot构建即可,无须这么复杂。仅仅是为了留下整体和细节一点的印象。
数据库连接池
JDBC还有一个没结尾,就是数据库连接池。mybatis 里面也会用到,这里统一介绍一下。首先来看看不使用连接池的请求性能,示例代码如下:
public static void main(String[] args) throws Exception {
final int requestNumber = 5;
CountDownLatch countDownLatch = new CountDownLatch(requestNumber);
UnpooledThread[] mts = new UnpooledThread[requestNumber];
for (int i = 0; i < mts.length; i++) {
mts[i] = new UnpooledThread(countDownLatch);
}
for (int j = 0; j < mts.length; j++) {
mts[j].start();
}
try {
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static class UnpooledThread extends Thread {
private CountDownLatch countDownLatch;
public UnpooledThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
long start = System.currentTimeMillis();
try {
Connection conn = DriverManager.getConnection(
"地址信息",
"账号",
"密码" );
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT id, username, password FROM sys_user LIMIT 1");
SysUser user = new SysUser();
// 遍历每一行数据
while (rs.next()) {
user.setId(rs.getString("id"));
user.setName(rs.getString("username"));
}
System.out.println(this.getName() + " " + user.toString());
System.out.println(this.getName() + ": " + (System.currentTimeMillis() - start));
} catch (SQLException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
}
输出如下:
Thread-3 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-3: 396
Thread-1 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-1: 399
Thread-4 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-4: 398
Thread-0 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-0: 399
Thread-2 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-2: 398
可以看到,每个请求消耗大约 390ms。为什么时间这么久呢?因为在建立数据库连接时,如上文所述,需要经过很多次的来回交互后才能建立连接,比如TCP的三次握手,数据库账号密码的认证等等。所以建立连接是非常耗时的,可以测试看下,在线程创建 Connection
对象后再次打印下消耗时间,即:
Connection conn = DriverManager.getConnection(
"地址信息",
"账号",
"密码" );
System.out.println(this.getName() + ": 建立连接消耗: " + (System.currentTimeMillis() - start));
......
输出如下:
Thread-0: 建立连接消耗: 381
Thread-3: 建立连接消耗: 394
Thread-2: 建立连接消耗: 396
Thread-0 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-0: 396
Thread-4: 建立连接消耗: 396
Thread-1: 建立连接消耗: 397
Thread-3 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-3: 403
Thread-2 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-2: 404
Thread-4 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-4: 407
Thread-1 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-1: 412
所以光是要创建 Connection
就要消耗大量的执行时间,而且 Connection 对象可以复用,所以数据库连接池就是缓存这些 Connection
对象。知道了这个原理后,我们简单设计一个数据库连接池来看看性能的提升:
使用一个队列来保存 Connection 对象即可,当线程使用完对象后,再把对象放回到队列中,连接池Pool类如下:
public static class Pool {
private static final int POOL_NUMBER = 3;
private static ConcurrentLinkedQueue<Connection> pool = new ConcurrentLinkedQueue<>();
private static Semaphore sem = new Semaphore(POOL_NUMBER);
static {
try {
for (int i = 0; i < POOL_NUMBER; i++) {
Connection conn = DriverManager.getConnection(
"地址信息",
"账号",
"密码" );
pool.add(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public Connection pop() {
try {
sem.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
Connection conn = pool.poll();
System.out.println("使用数据库连接:" + conn.hashCode());
return conn;
}
public void push(Connection conn) {
pool.add(conn);
sem.release();
}
}
测试方法如下:
public static void main(String[] args) throws Exception {
final int requestNumber = 7;
CountDownLatch countDownLatch = new CountDownLatch(requestNumber);
Pool pool = new Pool();
PooledThread[] mts = new PooledThread[requestNumber];
for (int i = 0; i < mts.length; i++) {
mts[i] = new PooledThread(pool, countDownLatch);
}
for (int j = 0; j < mts.length; j++) {
mts[j].start();
}
try {
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static class PooledThread extends Thread {
private CountDownLatch countDownLatch;
private Pool pool;
public PooledThread(Pool pool, CountDownLatch countDownLatch) {
this.pool = pool;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
long start = System.currentTimeMillis();
Connection conn = null;
try {
conn = pool.pop();
System.out.println(this.getName() + ": 获取连接消耗: " + (System.currentTimeMillis() - start));
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT id, username, password FROM sys_user LIMIT 1");
SysUser user = new SysUser();
// 遍历每一行数据
while (rs.next()) {
user.setId(rs.getString("id"));
user.setName(rs.getString("username"));
}
pool.push(conn);
System.out.println(this.getName() + " " + user.toString());
System.out.println(this.getName() + ": " + (System.currentTimeMillis() - start));
} catch (SQLException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
}
输出结果如下:
使用数据库连接:49123477
使用数据库连接:448026214
Thread-0: 获取连接消耗: 0
使用数据库连接:1698188942
Thread-2: 获取连接消耗: 0
Thread-1: 获取连接消耗: 0
Thread-2 SysUser(id=1140466515787780089, name=admin, password=null)
使用数据库连接:49123477
Thread-5: 获取连接消耗: 11
Thread-1 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-1: 11
使用数据库连接:1698188942
Thread-2: 11
Thread-3: 获取连接消耗: 11
Thread-0 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-0: 12
使用数据库连接:448026214
Thread-6: 获取连接消耗: 12
Thread-3 SysUser(id=1140466515787780089, name=admin, password=null)
使用数据库连接:1698188942
Thread-4: 获取连接消耗: 30
Thread-3: 30
Thread-5 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-5: 30
Thread-6 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-4 SysUser(id=1140466515787780089, name=admin, password=null)
Thread-6: 44
Thread-4: 44
其中,“使用数据库连接:49123477” ,数据表示每个连接对象的唯一ID,可以看到三个 Connection 对象的ID分别为:
49123477、448026214、1698188942。
然后输出中这三个ID依次打印出来,也表示了调用的情况。最终的结果可以看到,使用连接池后,每个请求的消耗时间大大降低了。
mybatis 配置使用过程解析
这里我们不介入spring等依赖,而是直接使用 mysql、mybatis来完成数据库的交互和对象映射。
在 pom 中引入jar包:
<!--这是前文引入的-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--这是新引入的Mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
Tips,如果想知道 pom 依赖怎么搜索,以下是小方法:直接在浏览器中输入 mybatis maven ,找到网址是:https://mvnrepository.com/ 的搜索链接,这个网站就是maven的中央仓库网站,默认工程里面的依赖都是从这个maven中央仓库下载的。点进去后依次找到 mybatis 的某个版本,直接复制下面的maven依赖标签即可。
首先,来看下测试的 main 方法:
public static void main(String[] args) {
try (SqlSession session = MybatisFactory.getSqlSessionFactoryInstatnce().openSession()) {
TestMapper mapper = session.getMapper(TestMapper.class);
SysUser user = mapper.selectUser();
System.out.println(user.toString());
}
}
输出结果如下:
SysUser(id=1140466515787780089, name=null, password=9d947539b4374770df2a52f6e357829d3e625cd78bc05451218e7b5b3361000b)
可以看到,同样能够请求到数据(这里和上文的不同在于密码我们也映射了,但是name为null,最后会说明)。
像一般的网络请求处理业务,都是先构造好交互环境,然后环境中分为会话信息,最后交换数据。我们的API请求处理也类似,先根据配置信息创建好网络环境,然后针对每个请求创建一个会话(包括线程以及本次请求的其他信息),每个会话中再处理数据交互。概念结构如下:
现在,我们来拆解一下这个main方法。
环境构建
很明显,SqlSession
就是会话对象,那么前述条件肯定是和环境相关了。回看 mybatis 官方文档,这个入门篇前面的部分描述了关于环境的关键点:
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。
这句话可以理解成:会话所基于的环境的环境信息,就是通过解析预先的配置后生成的,SqlSessionFactory 就可以当做是环境。
所以,我们就要去构造这个 Factory,那么怎么构造呢?接着看文档中给出的示例代码:
String resource = "org/mybatis/example/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
就是通过 SqlSessionFactoryBuilder
去解析配置文件后创建 Factory。在根据文档末尾的生命周期的说明:
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。… 因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
文档中对于 SqlSessionFactoryBuilder 的生命周期也有所描述,如下:
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但是最好还是不要让其一直存在,以保证所有的 XML 解析资源可以被释放给更重要的事情。
所以 SqlSessionFactoryBuilder 就在方法中创建一下作为跳板获取到最终的 SqlSessionFactory 就可以了,不要一直占用XML解析资源。
结合上述几点,最终构建Factory的代码如下,以单例模式创建工厂类:
public class MybatisFactory {
/**配置文件,默认回去 classpath 下搜索,也就是工程的 resource 目录下*/
private static final String resource = "mybatis-config.xml";
private MybatisFactory() {
}
public static SqlSessionFactory getSqlSessionFactoryInstatnce() {
return Builder.sqlSessionFactory;
}
private static class Builder {
private static SqlSessionFactory sqlSessionFactory = null;
static {
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
}
}
那么,这个环境的配置文件到底长什么样呢?示例如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--这个setting的意思是去掉框架本身的日志打印,太多了-->
<settings >
<setting name="logImpl" value="NO_LOGGING"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://你的数据库ip/数据库名"/>
<property name="username" value="账号"/>
<property name="password" value="密码"/>
</dataSource>
</environment>
</environments>
</configuration>
是不是和JDBC中的数据源配置非常像?配置文件格式是XML,和前文的JSON格式类似,是一种标签化语言。它也遵循一定的格式(语法)规范来表现意图,Factory 创建的时候会读取这个配置的内容。
可以看到标签 dataSource
的 type 是 POOLED
,Pooled的意思是启用数据库连接池。由上文可知和mysql建立一次连接需要很多交互过程,性能浪费。所以需要将连接对象也就是上篇文章中的 Connection 对象缓存起来,达到一种复用的目的,后续的sql请求语句都可以复用这些连接对象。
会话构建
环境的准备工作好了,下一步就是创建会话对象了。
同理,先来看下 SqlSession 的生命周期:
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域
文档中也给到了示例代码:
try (SqlSession session = sqlSessionFactory.openSession()) { // 你的应用逻辑代码 }
在你的所有的代码中一致地使用这种模式来保证所有数据库资源都能被正确地关闭。
从文档中的注释可以看出,SqlSession对象的某些方法中会去数据库连接池获取与释放连接对象。所以每个 SqlSession就可以类比为上文中演示的请求SQL的线程。
数据交互
到了最重要的数据交互,这也是 mybatis 的核心,即方便的执行SQL和获取结果。mybatis 框架重要的组成部分就是 映射器(Mapper),其由两部分组成:映射器接口和映射器配置(实例)。
映射器接口声明数据操作方法,比如 selectUser()
,而映射器配置声明具体需要执行的SQL语句,以及请求参数和返回对象。映射器配置的实现又有两种方式:基于注解的配置和基于xml的配置。我们先来看下 xml 的配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.thingcom.webtest.mybatis.TestMapper">
<select id="selectUser" resultType="com.thingcom.webtest.charger.SysUser">
SELECT id, username, password FROM sys_user LIMIT 1
</select>
<insert>
....
</insert>
</mapper>
其中,mapper 标签里面的 select 标签,其包括的就是具体的SQL执行语句,我们可以看到 select 标签的id 是selectUser
,resultType
是 SysUser 对象。
我们的 TestMapper 接口定义如下,即映射器接口定义如下:
public interface TestMapper {
SysUser selectUser();
}
可以看到,接口名和 xml 中的 mapper 标签的 namespace 属性一样。然后接口方法 selectUser 方法名也和 select 标签的 id 属性对应,返回类型和上文用到的 SysUser 类一样。所以当我们执行如下方法时:
TestMapper mapper = session.getMapper(TestMapper.class);
会话会根据传入的接口Class对象,利用反射创建出映射器接口的实例。然后执行:
SysUser user = mapper.selectUser();
会最终执行映射器配置中的SQL语句,并创建 SysUser的实例,将SQL返回值自动映射到 SysUser 对象的属性上。如果我们再写查找用户其他信息的方法时,可以在 映射器配置Mapper文件中添加一个新的 select 标签。
当然还有 insert,update,delete这几个映射标签。
至于这个xml映射文件应该放在哪里,需要先在 mybatis-config文件里进行配置,补充后的配置如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings >
<setting name="logImpl" value="NO_LOGGING"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://你的数据库ip/数据库名"/>
<property name="username" value="账号"/>
<property name="password" value="密码"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/TestMapper.xml"/>
</mappers>
</configuration>
也就是最下面的 mapper 标签决定的,即放在 resource/mapper/目录下。一般来说,我们会对每个数据表定义各自的映射器。
文档中同样提到了映射器的生命周期:
映射器是一些由你创建的、绑定你映射的语句的接口。映射器接口的实例是从 SqlSession 中获得的。因此从技术层面讲,任何映射器实例的最大作用域是和请求它们的 SqlSession 相同的。尽管如此,映射器实例的最佳作用域是方法作用域。 也就是说,映射器实例应该在调用它们的方法中被请求,用过之后即可丢弃。
所以代码中也是按照同样的要求去写的。
小结
通过整个的学习,我们了解了数据库连接池的意义;mybatis的环境构建-会话构建-数据交互的流程和细节。可以进一步自行学习其他insert等标签的使用,多加练习以掌握mybatis的映射诀窍。
ORM 中,类就相当于数据表;类的实例对象就相当于数据表中的每一行;对象的属性就相当于每一列字段。
这里抛出一个小问题,也是下篇文章会讲到的:关于mybatis的映射细节。上文xml配置中,sql语句为:
<select id="selectUser" resultType="com.thingcom.webtest.charger.SysUser"> SELECT id, username, password FROM sys_user LIMIT 1 </select>
那么,为什么最终映射出来的 SysUser 对象的name 属性没有具体数据呢?