概述
- 表现层(UI):直接跟前端打交互(一是接收前端ajax请求,二是返回json数据给前端)
- 业务逻辑层(BLL):一是处理表现层转发过来的前端请求(也就是具体业务),二是将从持久层获取的数据返回到表现层。
- 数据访问层(DAL):直接操作数据库完成CRUD,并将获得的数据返回到上一层(也就是业务逻辑层)。
- Java持久层框架:
-
- MyBatis
- Hibernate(实现了JPA规范)
- jOOQ
- Guzz
- Spring Data(实现了JPA规范)
- ActiveJDBC
JDBC的不足
示例一:
// ......
// sql语句写在java程序中
String sql = "insert into t_user(id,idCard,username,password,birth,gender,email,city,street,zipcode,phone,grade) values(?,?,?,?,?,?,?,?,?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
// 繁琐的赋值:思考一下,这种有规律的代码能不能通过反射机制来做自动化。
ps.setString(1, "1");
ps.setString(2, "123456789");
ps.setString(3, "zhangsan");
ps.setString(4, "123456");
ps.setString(5, "1980-10-11");
ps.setString(6, "男");
ps.setString(7, "[email protected]");
ps.setString(8, "北京");
ps.setString(9, "大兴区凉水河二街");
ps.setString(10, "1000000");
ps.setString(11, "16398574152");
ps.setString(12, "A");
// 执行SQL
int count = ps.executeUpdate();
// ......
示例二:
// sql语句写在java程序中
String sql =
"select id,idCard,username,password,birth,gender,email,city,street,zipcode,phone,grade from t_user";
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
List<User> userList = new ArrayList<>();
// 思考以下循环中的所有代码是否可以使用反射进行自动化封装。
while(rs.next()){
// 获取数据
String id = rs.getString("id");
String idCard = rs.getString("idCard");
String username = rs.getString("username");
String password = rs.getString("password");
String birth = rs.getString("birth");
String gender = rs.getString("gender");
String email = rs.getString("email");
String city = rs.getString("city");
String street = rs.getString("street");
String zipcode = rs.getString("zipcode");
String phone = rs.getString("phone");
String grade = rs.getString("grade");
// 创建对象
User user = new User();
// 给对象属性赋值
user.setId(id);
user.setIdCard(idCard);
user.setUsername(username);
user.setPassword(password);
user.setBirth(birth);
user.setGender(gender);
user.setEmail(email);
user.setCity(city);
user.setStreet(street);
user.setZipcode(zipcode);
user.setPhone(phone);
user.setGrade(grade);
// 添加到集合
userList.add(user);
}
JDBC的不足:
- 修改SQL就要改动Java代码,违背OCP原则
- 给 ? 占位符传值太繁琐
- 结果集封装为Java对象太繁琐
Mybatis框架
- MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis。2013年11月迁移到Github。
- iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAOs)。
ORM:对象关系映射 Object Relational Mapping;ORM就是将数据库表中的一条记录映射为Java中的一个对象
一张表就对应了一个Java类,一个字段就对应了一个属性; 数据库中一条记录和Java对象的关系就是 ORM
- MyBatis属于半自动化ORM框架,因为MyBatis中SQL语句是需要自己写的
- Hibernate属于全自动化的ORM框架,不需要程序员手动写SQL语句
MyBatis框架特点:
- 支持定制化 SQL、存储过程、基本映射以及高级映射
- 避免了几乎所有的 JDBC 代码中手动设置参数以及获取结果集
- 支持XML开发,也支持注解式开发。【为了保证sql语句的灵活,所以mybatis大部分是采用XML方式开发。】
- 将接口和 Java 的 POJOs(Plain Ordinary Java Object,简单普通的Java对象)映射成数据库中的记录
- 体积小好学:两个jar包,两个XML配置文件。
- 完全做到sql解耦合。
- 提供了基本映射标签。
- 提供了高级映射标签。
- 提供了XML标签,支持动态SQL的编写。
数据库表的设计
第一个程序
放在resources目录下的资源等同于放在类的根路径下
开发步骤:
-
packaging:jar
-
引入依赖
- mybatis
- mysql
-
从XML中构建SqlSessionFactory对象
在MyBatis中有一个对象SqlSessionFactory,这个对象的创建需要XML配置文件
mybatis核心配置文件 mybatis-config.xml,一般放在类的根路径下
配置文件参见:mybatis中文网
另一个核心配置文件:xxxMapper.xml,用来编写SQL语句的配置文件
mybatis-config.xml只有一个,XxxMapper.xml一张数据库表一个
-
XxxMapper.xml配置文件
-
在mybatis-config的mapper标签关联到CarMapper配置文件
-
Mybatis程序
在Mybatis中执行SQL的对象是SqlSession,代表JVM和数据库之间的一次会话
获取SqlSession对象,就要先获取SqlSessionFactory对象,通过工厂模式就可以获取到SqlSession对象;
获取SqlSessionFactory对象,要先获取SqlSessionFactoryBuilder对象,通过build方法就可以获取SqlSessionFactory对象
一般情况下,一个数据库一个SqlSessionFactory对象
SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession
public class MyBatisIntroductionTest {
public static void main(String[] args) throws IOException {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// InputStream is = Thread.currentThread().getContextClassLoader()
// .getResourceAsStream("mybatis-config.xml");
// 获取系统类加载器
// InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");//从类的根路径下获取资源
//输入流指向Mybatis-config核心配置文件
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
//获取的session默认是不自动提交的
SqlSession sqlSession = sqlSessionFactory.openSession();
//参数是SQL语句的id
int count = sqlSession.insert("insertCar");
System.out.println("插入了 " + count + " 条记录");
//手动提交
sqlSession.commit();
}
}
细节:
-
XxxMapper.xml中SQL语句的 ; 可以省略
-
resource方式加载的资源,大多数都是在类的根路径下进行加载的
-
Mybatis工具类的Resources:
-
mybatis-config中mapper还有一个url属性,是从绝对路径下加载资源
注意有前缀
事务管理器
默认openSession获取的session对象是不会自动提交的,需要手动提交
在mybatis-config中事务管理器 transactionManager 属性 :
可以指定两个值,对应两种事务管理器
-
JDBC :mybatis框架直接使用原生JDBC管理事务,创建JDBCTransaction对象
//sqlSessionFactory.openSession(); conn.setAutoCommit(false); //sqlSession.commit() 还是会执行这个代码 conn.commit();
-
MANAGED:mybatis不再负责事务管理,交给其他容器(Spring)负责
对于当前的Mybatis框架学习,如果配置为MANAGED,事务是没有其他容器来管理的,没有人管理事务表示事务就从来没有开启
对于SQL语句的执行,一定是获取了Connection对象后获取PreparedStatement进行执行的,对21行代码进行调试:
此时的autoCommit是false
进入newTransaction
方法:
创建JDBC事务管理器最终一定会执行这个方法:
其中:
关闭了自动提交,最终需要手动调用sqlSession对象的commit方法提交
也就是JdbcTransaction以openSession()
获取的SqlSession对象的autoCommit被指定为false
也可以在openSession时指定参数true:
debug:
就会在上文中通过工厂设计模式创建JDBCTransaction,在JDBCTransaction中openConnection最终会调用:
也就是说,如果为true就不会进入第一个if判断,也就不会设置JDBC的autoCommit机制,默认为true
如果在openSession时:
SqlSession sqlSession = sqlSessionFactory.openSession(true);
表示没有开启事务,因为就没有执行到conn.setAutoCommit(false)
,执行任何一条DML语句都会提交,这种方式是不建议的
如果设置为MANAGED,事务是没有任何人管理的(auto = true),表示事务就没有开启;任何DML语句都会自动提交
只要autoCommit为true,就是没有开启事务
事务管理接口:Transaction的两个实现类
单元测试和日志
- 单元测试
public class CarMapperTest {
@Test
public void testInsertCar(){
SqlSession sqlSession = null;
try {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources
.getResourceAsStream("mybatis-config.xml"));
sqlSession = sqlSessionFactory.openSession();
int count = sqlSession.insert("insertCar");
sqlSession.commit();
System.out.println("插入了 : " + count);
} catch (IOException e) {
e.printStackTrace();
if (sqlSession != null) {
sqlSession.rollback();
}
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
}
- 日志
引入日志是为了查看mybatis中具体执行的SQL文件
mybatis常见的日志组件:
对于mybatis-config中,标签的顺序是有要求的:
这个要求是dtd约束指定的:
<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>
- 启用标准日志组件
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
使用Mybatis框架对标准日志的实现,但是配置不够灵活,可以集成其他日志组件,例如logback(实现SLF4J)等
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 209429254.
==> Preparing: insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,'1003','AD',30.0,'2000-10-11','燃油车');
==> Parameters:
<== Updates: 1
插入了 : 1
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@c7ba306]
Returned connection 209429254 to pool.
- 集成第三方日志组件,需要引入依赖
logback实现了SLF4J标准,引入logback依赖
引入logback必需的xml配置文件:logback.xml或者logbacktest.xml,必须放在类的根路径下
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!--mybatis log configure-->
<logger name="com.apache.ibatis" level="TRACE"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
如果开启的是第三方日志,不需要设置setting
工具类
public class SQLSessionUtil {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
private SQLSessionUtil() {
}
public static SqlSession openSession(){
return sqlSessionFactory.openSession();
}
}
Mybatis完成CRUD
CRUD:Create 增 、 Retrieve 查 、 Update 改 、 Delete 删
- insert
值不能固定在配置文件中,一定是前端提交过来的数据,将数据传递给SQL语句
在JDBC中占位符是 ? ,在Mybatis中和 ? 等效的写法是#{}
insert第二个参数是一个对象,这个对象就是用来封装数据的,这就是ORM,这个对象会映射为数据库中的一条记录
在CarMapper.xml文件中:
执行:
15:36:17.195 [main] DEBUG eun.insertCar - ==> Preparing: insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,?,?,?,?,?);
15:36:17.222 [main] DEBUG eun.insertCar - ==> Parameters: 1111(String), BYD(String), 10.0(Double), 2020-11-11(String), 新能源车(String)
如果指定键时出错:
15:38:21.253 [main] DEBUG eun.insertCar - ==> Preparing: insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,?,?,?,?,?);
15:38:21.278 [main] DEBUG eun.insertCar - ==> Parameters: 1111(String), BYD(String), 10.0(Double), 2020-11-11(String), null
传递过去的就是空值NULL,map集合的get方法拿到的null,如果数据库不允许为NULL就会报错
但是数据库中的一条记录,应该对应一个个体,使用HashMap的表现力不够强
- 使用POJO类给SQL语句的占位符传值
public class Car {
//包装类可以防止NULL的问题
private Long id;
private String carNum;
private String brand;
private Double guidePrice;
private String produceTime;
private String carType;
}
SQL语句中对应的就是POJO类的属性名
15:48:32.061 [main] DEBUG eun.insertCar - ==> Preparing: insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,?,?,?,?,?);
15:48:32.087 [main] DEBUG eun.insertCar - ==> Parameters: 3333(String), BYDQ(String), 30.0(Double), 2020-11-11(String), 电车(String)
如果SQL中的属性名写错了:
There is no getter for property named 'errorName' in 'class com.eun.pojo.Car'
找不到这个属性名对应的get方法,所以此处并不是属性名,而是get方法去掉get并将属性名第一个字母变为小写后的字符串
如果在POJO类中提供getErrorName方法:
运行成功
getCarType() ---> carType
- delete
需求:根据id删除数据
将id = 59 的数据删除
delete方法的第二个参数是Object类型,此时会自动封装为对应的包装类,然后传递给delete方法的参数id
16:10:37.415 [main] DEBUG eun.deleteById - ==> Preparing: delete from t_car where id = ?;
16:10:37.442 [main] DEBUG eun.deleteById - ==> Parameters: 59(Integer)
如果占位符只有一个,#{}
里面的内容可以随便写,但是最好符合规范
- update
根据id修改某条记录
此时根据id修改,创建的POJO对象id就不能传入null
- select
根据主键查一个
问题:返回值应该是Object,强制类型转换为Car,但是此处没有经过强制类型转换也没有报错
mybatis执行了select语句后,一定会返回一个结果集对象:ResultSet,接下来应该从ResultSet中取出数据封装为java对象
测试报错:
A query was run and no Result Maps were found for the Mapped Statement 'eun.selectById'. Its likely that neither a Result Type nor a Result Map was specified.
已运行查询,但是未找到映射语句'eun.selectById'的结果映射,很可能既没有指定结果类型也没有指定结果映射
没有指定返回值的结果类型,通过resultType指定将查询结果集封装成什么类型的对象:
resultType: 全限定类名
查询的结果:
数据库中的字段名:
字段名和属性名不同的就是null
解决办法:as 起别名
<select id="selectById" resultType="com.eun.pojo.Car">
select id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from
t_car
where id = #{id};
</select>
<!--car = Car{id = 18, carNum = 9999, brand = BYDA, guidePrice = 300.0, produceTime = 2020-11-11, carType = 新能源车}-->
查询所有:
<select id="selectAll" resultType="com.eun.pojo.Car">
select id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car
</select>
@Test
public void testAll(){
SqlSession sqlSession = SQLSessionUtil.openSession();
List<Car> cars = sqlSession.selectList("selectAll");//指定为List
cars.forEach(System.out::println);
sqlSession.commit();
sqlSession.close();
}
SQLMapper的namespace
如果在另一个StudentMapper.xml文件中也有id = selectAll的查询语句,将StudentMapper.xml也注册到mybatis-config.xml中
此时执行selectAll这条语句就会报错:
selectAll is ambiguous in Mapped Statements collection (try using the full name including the namespace, or rename one of the entries)
selectAll 是 无效的 请尝试使用命名空间的全名或者重命名
因为mybatis不确定执行CarMapper还是StudentMapper中的selectAll,需要使用命名空间进行区分:
命名空间.id
mapper标签的namespace属性是为了防止id冲突,标准写法就是命名空间.id
Mybatis核心配置文件
配置多个环境:
默认环境:创建SqlSessionFactory时没有指定环境的话默认使用的环境
创建SqlSessionFactory的build方法:
@Test
public void testEnvironment() throws IOException {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
//创建指定环境
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder
.build(Resources.getResourceAsStream("mybatis-config.xml"), "mybatis");
//创建默认环境
SqlSessionFactory sqlSessionFactoryDefault = sqlSessionFactoryBuilder
.build(Resources.getResourceAsStream("mybatis-config.xml"));
}
默认环境就是environments标签的default属性指定的环境
工具类:
public class SQLSessionUtil {
private static final SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = sqlSessionFactoryBuilder
.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
private SQLSessionUtil() {
}
public static SqlSession openSession(){
return sqlSessionFactory.openSession();
}
public static SqlSessionFactory environment(String environment){
try {
sqlSessionFactory = sqlSessionFactoryBuilder
.build(Resources.getResourceAsStream("mybatis-config.xml"),environment);
} catch (IOException e) {
e.printStackTrace();
}
return sqlSessionFactory;
}
}
dataSource数据源的配置
dataSource被称为数据源,为程序提供Connection对象,数据源实际上是一套规范,JDK中有这套规范,在javax.sql.DataSource下
我们可以根据这个接口写数据源组件,实现接口中所有的方法
比如可以定义一个自己的数据库连接池,常见的数据源组件:druid、c3p0、dbcp
dataSource的type属性就是指定哪个数据库连接池,指明用何种方式获取Connection对象
type属性值三选一:
- UNPOOLED:不使用数据库连接池技术,每一次请求都创建新的Connection对象
- POOLED:使用Mybatis自己实现的数据库连接池
- JNDI:集成第三方的数据库连接池
JNDI是一套规范,大部分WEB容器都实现了这套规范,例如Tomcat、Jetty、WebLogic、WebSphere
JNDI是java命名目录接口
UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。UNPOOLED 类型的数据源仅仅需要配置以下 5 种属性:
driver
– 这是 JDBC 驱动的 Java 类全限定名(并不是 JDBC 驱动中可能包含的数据源类)。url
– 这是数据库的 JDBC URL 地址。username
– 登录数据库的用户名。password
– 登录数据库的密码。defaultTransactionIsolationLevel
– 默认的连接事务隔离级别。defaultNetworkTimeout
– 等待数据库操作完成的默认网络超时时间(单位:毫秒)。查看java.sql.Connection#setNetworkTimeout()
的 API 文档以获取更多信息。作为可选项,你也可以传递属性给数据库驱动。只需在属性名加上“driver.”前缀即可,例如:
driver.encoding=UTF8
这将通过 DriverManager.getConnection(url, driverProperties) 方法传递值为
UTF8
的encoding
属性给数据库驱动。 POOLED– 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。
除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源:
poolMaximumActiveConnections
– 在任意时间可存在的活动(正在使用)连接数量,默认值:10poolMaximumIdleConnections
– 任意时间可能存在的空闲连接数。poolMaximumCheckoutTime
– 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)poolTimeToWait
– 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。poolMaximumLocalBadConnectionTolerance
– 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过poolMaximumIdleConnections
与poolMaximumLocalBadConnectionTolerance
之和。 默认值:3(新增于 3.4.5)poolPingQuery
– 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息。poolPingEnabled
– 是否启用侦测查询。若开启,需要设置poolPingQuery
属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。poolPingConnectionsNotUsedFor
– 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。 JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。这种数据源配置只需要两个属性:
initial_context
– 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。data_source
– 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。和其他数据源配置类似,可以通过添加前缀“env.”直接把属性传递给 InitialContext。比如:
env.encoding=UTF8
这就会在 InitialContext 实例化时往它的构造方法传递值为
UTF8
的encoding
属性。
如果使用JNDI的方式,只能在dataSource中配置上述的两个属性,Tomcat服务器实现了JNDI规范,druid连接池可以配置到Tomcat服务器上,Tomcat可以对外提供一个连接池的名字,只需要在这里获取JNDI上下文的路径,这个配置方式只是为了让我们能在Tomcat服务器中使用Mybatis
Tomcat可以集成Druid数据库连接池,只需要提供JNDI上下文名字,Mybatis就可以使用集成的数据库连接池
- 配置具体的连接池参数
正常使用连接池,有很多参数需要设置,具体的参数配置需要根据当前业务情况进行测试
poolMaximumActiveConnections
:最大连接数,默认值是10
测试:对于第11个创建sqlSession请求:
16:07:39.236 [main] DEBUG car.insertCar - <== Updates: 1
16:07:39.236 [main] DEBUG org.apache.ibatis.transaction.managed.ManagedTransaction - Opening JDBC Connection
16:07:39.236 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Waiting as long as 20000 milliseconds for connection.
//20s后:
16:07:59.251 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Claimed overdue connection 1327871893.
对于尝试获取数据库连接的间隔可以用 poolTimeToWait
标签配置
<property name="poolTimeToWait" value="2000"/>
每隔2s打印日志并尝试重新获取连接对象,并不是连接超时时间
连接超时时间:poolMaximumCheckoutTime
,强行让某个连接空闲
<property name="poolMaximumCheckoutTime" value="10000"/>
properties标签
java.util.Properties类是一个Map集合,key和value都是String类型
properties标签中可以配置很多属性
使用时就可以:
但是没必要这样写,可以将这些内容都抽取到jdbc.properties中:
使用时:
GodBatis
解析mybatis-config.xml配置文件
Node selectSignalNode(String xpath) // 获取指定路径下的一个子元素
Element element(String s) // 获取当前元素下指定的子元素
Element elements() // 获取当前元素下所有的子元素
根据environments标签的default属性值获取对应的environment子标签
首先要获取default标签的值:
public void testParseMyBatisConfigXML() throws IOException, DocumentException {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(Resources.getResourceAsStream("mybatis-config.xml"));
Element rootElement = document.getRootElement();
//xpath : 标签路径匹配,快速定位XML文件中的元素
//从根下找configuration标签,再找environments子标签
String xpath = "/configuration/environments";
//Element是Node类的子类,获取到的是Node类型引用 Element类型对象
Element environments = (Element) rootElement.selectSingleNode(xpath);
String defaultEnvironmentId = environments.attributeValue("default");
System.out.println(defaultEnvironmentId);//development
}
获取environments标签id属性对应的environment
- 思路一:获取environments下所有子标签,遍历匹配
//xpath : 标签路径匹配,快速定位XML文件中的元素
//从根下找configuration标签,再找environments子标签
String xpath = "/configuration/environments";
//Element是Node类的子类
Element environments = (Element) rootElement.selectSingleNode(xpath);
String defaultEnvironmentId = environments.attributeValue("default");
List<Element> elements = environments.elements();
for (Element element : elements) {
if (defaultEnvironmentId.equals(element.attributeValue("id"))){
getInfos(element);
}
}
- 思路二:
selectSignalNode
,直接获取id属性 = defaultEnvironmentId的元素
//Element是Node类的子类
Element environments = (Element) rootElement.selectSingleNode(xpath);
String defaultEnvironmentId = environments.attributeValue("default");
xpath = "/configuration/environments/environment[@id='" + defaultEnvironmentId + "']";
// /configuration/environments/environment[@id='development']
Element environment = (Element) rootElement.selectSingleNode(xpath);
获取environment的子节点
//transactionManager 子节点
Element transactionManager = environment.element("transactionManager");
String transactionType = transactionManager.attributeValue("type");
System.out.println("transactionType = " + transactionType);
//dataSource
Element dataSource = environment.element("dataSource");
String dataSourceType = dataSource.attributeValue("type");
System.out.println("dataSourceType = " + dataSourceType);
//获取dataSource下所有的子节点
List<Element> properties = dataSource.elements();
properties.forEach(property ->
System.out.println(property.attributeValue("name") + "=" + property.attributeValue("value"))
);
//获取任意位置的所有mapper标签
xpath = "//mapper";
List<Node> mappers = document.selectNodes(xpath);
mappers.stream().map(node -> (Element) node)
.forEach(element -> System.out.println(element.attributeValue("resource")));
解析XxxMapper.xml配置文件
//获取namespace
Element mapper = (Element) document.selectSingleNode("/mapper");
String namespace = mapper.attributeValue("namespace");
System.out.println("namespace = " + namespace);
//获取mapper下所有子节点
List<Element> elements = mapper.elements();
elements.forEach(element -> {
String id = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String sql = element.getTextTrim().replaceAll("#\\{[0-9A-Za-z_$]*}?","?");
System.out.println("id=" + id + " resultType=" + resultType + " sql=" + sql);
});
- 参照第一个Mybatis程序定义类
public class MyBatisIntroductionTest {
public static void main(String[] args) throws IOException {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
int count = sqlSession.insert("insertCar");
sqlSession.commit();
}
}
先定义Resources工具类:
/**
* 工具类,完成类路径下的资源的加载
*/
public class Resources {
private Resources() {
}
/**
* 从类路径加载资源
* @param resource 类路径下资源文件
* @return 指向类路径的输入流
*/
public static InputStream getResourceAsStream(String resource){
return ClassLoader.getSystemClassLoader().getResourceAsStream(resource);
}
}
SqlSessionFactoryBuilder:
/**
* SqlSessionFactory 的构建器对象
* 通过build方法解析godbatis-config.xml文件,创建SqlSessionFactory对象
*/
public class SqlSessionFactoryBuilder {
public SqlSessionFactoryBuilder() {
}
/**
* 解析godbatis-config.xml文件,构建创建SqlSessionFactory对象
* @param is 指向godbatis-config.xml文件的输入流
* @return 返回SqlSessionFactory对象
*/
public SqlSessionFactory build(InputStream is){
return null;
}
}
这时应该先定义SqlSessionFactory类:
/**
* SqlSessionFactory对象:
* 一个数据库对应一个SqlSessionFactory对象
* 可以获取SqlSession对象,开启多个对话
*/
public class SqlSessionFactory {
/**
* 事务管理器属性
*/
private Transaction transaction;
/** dataSource 交给数据源*/
/**
* Mapper
*/
private Map<String,MappedStatement> mappedStatements;
}
一个SqlSessionFactory对应一个数据库,通过SqlSessionFactory可以获取多个SqlSession对象
从配置文件中分析,SqlSessionFactory应该有:transactionManager属性、mapper属性、dataSource数据源属性
- Mapper属性:SQL语句的Map集合,sql语句还需要定义一个resultType属性,此处需要额外给SQL语句定义一个类
Map集合:
SQLID | MappedStatement |
---|---|
“user.selectById” | MappedStatement |
- Transaction接口至少提供三个方法:
/**
* 事务管理器接口
* 所有事务管理器都应该实现这个规范
* 提供控制事务的方法
*/
public interface Transaction {
/**
* 提交事务
*/
void commit();
/**
* 回滚事务
*/
void rollback();
/**
* 关闭事务
*/
void close();
}
对应有的JDBCTransaction、ManagedTransaction两个实现类
在JDBCTransaction中控制事务要还是要使用连接对象Connection的commit、rollback、close方法;而连接对象Connection是从DataSource数据源中获取的,数据源是提供连接对象的中心
所以数据源属性是定义在JDBCTransaction对象中的,在SqlSessionFactory对象中可以删除这个属性,在SqlSessionFactory中可以通过Transaction属性直接获取到数据源
对于数据源,JDK提供了规范:javax.sql.DataSource
,为接口提供三个实现类:UnPooledDatasource、PooledDataSource、JNDIDataSource
在数据源中获取数据库连接,需要先注册驱动,而驱动只需要注册一次,可以在UnPooledDataSource的构造方法中注册,因为SqlSessionFactory对象只会创建一次(对应一张数据库表),所以UnPooledDataSource的构造方法也会注册一次
driver不必定义为属性,因为driver只会使用一次,而url、username、password会使用多次
在JDBCTransaction中,控制事务需要获取Connection对象:
所以在此处应该定义一个成员变量Connection,这个Connection是单例模式的:
这个方法应该放在Transaction接口中
SqlSession执行SQL语句需要用到这个Connection对象,所以要在JDBCTransaction中提供一个public方法对外提供Conneciton对象
Connection是dataSource提供的,由transaction提供Connection对象更合适
现在回到SqlSessionFactoryBuilder中:
需要解析godbatis-config.xml配置文件,获取这两个对象
获取Transaction对象:
private Transaction getTransaction(Document document){
Element environments = (Element) document.selectSingleNode("/configuration/environments");
String defaultValue = environments.attributeValue("default");
Element environment = (Element) document.selectSingleNode("/configuration/environments/environment[@id='" + defaultValue + "']");
Element transactionManager = environment.element("transactionManager");
String transactionManagerType = transactionManager.attributeValue("type");
Element dataSourceElem = environment.element("dataSource");
DataSource dataSource = DataSourceFactory.getDataSource(dataSourceElem);
return TransactionFactory.getTransaction(transactionManagerType, dataSource);
}
第8行获取DataSource的方法:
public class DataSourceFactory {
public static DataSource getDataSource(Element dataSourceElem){
String dataSourceType = dataSourceElem.attributeValue("type");
List<Element> properties = dataSourceElem.elements();
Map<String,String> map = new HashMap<>();
//使用map集合存储数据
properties.forEach(property ->
map.put(property.attributeValue("name"),property.attributeValue("value")));
DataSource dataSource = null;
switch (dataSourceType){
case "POOLED" : dataSource = new PooledDataSource() ;break;
case "UNPOOLED" : dataSource = new UnPooledDataSource(map.get("driver"),
map.get("url"),
map.get("username"),
map.get("password")) ;break;
case "JNDI" : dataSource = new JNDIDataSource() ;break;
}
return dataSource;
}
}
获取mappedStatements对象:
private Map<String,MappedStatement> getMappedStatements(Document document){
Map<String,MappedStatement> map = new HashMap<>();
List<Node> nodes = document.selectNodes("//mapper");
List<String> resources = nodes.stream()
.map(node -> ((Element) node).attributeValue("resource")).toList();
for (String resource : resources) {
Document mapperDocument = null;
try {
//注意以流的方式传入
mapperDocument = new SAXReader().read(Resources.getResourceAsStream(resource));
} catch (DocumentException e) {
e.printStackTrace();
}
Element mapper = mapperDocument.getRootElement();
String namespace = mapper.attributeValue("namespace");
mapper.elements().forEach(element ->
map.put(namespace + "." + element.attributeValue("id"),
new MappedStatement(element.attributeValue("resultType"),element.getTextTrim()))
);
}
return map;
}
测试:
接下来就是sqlSessionFactory.openSession()
方法获取sqlSession对象
public SqlSession openSession(){
//开启会话的前提是Connection开启
transaction.openConnection();
//创建SqlSession对象
SqlSession sqlSession = new SqlSession();
return sqlSession;
}
SqlSession对象的行为:执行SQL语句,完成这个操作需要:
- connection对象,也就是此处的transaction对象(数据源对象在transaction对象中)
- mappedStatements对象,根据id获取SQL语句
所以需要给SqlSession对象传递这两个参数,而此时在SqlSessionFactory中有这两个数据:
在sqlSession中提供两个方法:selectOne、insert
insert方法的参数:
- sql语句的id,从mappedStatements中获取对应的SQL语句
- Object pojo,封装了要传入的参数
在mybatis中的sql语句:
insert into t_user values(#{id},#{name},#{age});
最终执行的sql语句:
insert into t_user values(?,?,?);
-
将
#{...}
转换为?
,获取PreparedStatement -
给
?
占位符传值,如何将Object类型的参数传递给占位符?,将哪个字段传递给哪个占位符?根据mybatis中的sql语句,截取
#{...}
中的字符内容,将字符内容转换为对应的get方法,依次反射调用pojo对象中对应的方法,将得到的结果按顺序传递给prep就可以了。
/**
* 向数据库中插入一条记录
* @param sqlId sql语句的id
* @param pojo 插入的数据
* @return
*/
public int insert(String sqlId,Object pojo){
int count = 0;
try {
String godbatisSql = this.sqlSessionFactory.getMappedStatements().get(sqlId).getSql();
String sql = godbatisSql.replaceAll("#\\{[0-9a-zA-Z_$]*}","?");
Connection connection = this.sqlSessionFactory.getTransaction().getConnection();
PreparedStatement prep = connection.prepareStatement(sql);
//给 ? 传值,假设都是String
//根据原sql语句,截取出属性名,反射调用pojo对象的get方法
List<String> methodNames = getMethodName(godbatisSql);
Class<?> clazz = pojo.getClass();
for (int i = 0; i < methodNames.size(); i++) {
String value = (String) clazz.getDeclaredMethod(methodNames.get(i)).invoke(pojo);
prep.setString(i + 1, value);
}
count = prep.executeUpdate();
} catch (SQLException | NoSuchMethodException |InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
return count;
}
private List<String> getMethodName(String godbatisSql) {
List<String> result = new ArrayList<>();
Pattern compile = Pattern.compile("#\\{.*?}");
Matcher matcher = compile.matcher(godbatisSql);
while (matcher.find()){
String str = matcher.group();
//截取大括号内部的内容
String field = str.substring(2, str.length() - 1);
char firstC = field.charAt(0);
if (firstC >= 'a' && firstC <= 'z'){
char[] charArray = field.toCharArray();
charArray[0] = (char) (firstC - 32);
field = new String(charArray);
}
result.add("get" + field);
}
//返回方法名,List中的顺序是按照Mapper文件中的SQL语句中的顺序定义的
return result;
}
其中,将首字符大写的另一种方法:
private List<String> getMethodName(String godbatisSql) {
List<String> result = new ArrayList<>();
Pattern compile = Pattern.compile("#\\{.*?}");
Matcher matcher = compile.matcher(godbatisSql);
while (matcher.find()){
String str = matcher.group();
String field = str.substring(2, str.length() - 1);
result.add("get" + field.toUpperCase().charAt(0) + field.substring(1));
}
return result;
}
- 实现selectOne方法:
mybatis中的SQL语句:
select * from t_user where id = #{id};
执行的SQL语句:
select * from t_user where id = ?;
查询到的结果:
mysql> select * from t_user where id = '001';
+-----+----------+------+
| id | name | age |
+-----+----------+------+
| 001 | zhangsan | 20 |
+-----+----------+------+
可以根据resultSet.getMetaData()
获取元数据,元数据:
通过反射机制获取resultType类型的字节码文件,再通过元数据获取查询结果集中的列名,拼接成对应的set方法
public Object selectOne(String sqlId,Object param){
MappedStatement mappedStatement = sqlSessionFactory.getMappedStatements().get(sqlId);
String godbatisSql = mappedStatement.getSql();
String sql = godbatisSql.replaceAll("#\\{[0-9a-zA-Z_$]*}","?");
Connection connection = this.sqlSessionFactory.getTransaction().getConnection();
String resultType = mappedStatement.getResultType();
Class<?> clazz = null;
Object obj = null;
try {
PreparedStatement prep = connection.prepareStatement(sql);
prep.setString(1, param.toString());
ResultSet resultSet = prep.executeQuery();
if (resultSet.next()) {
clazz = Class.forName(resultType);
obj = clazz.getConstructor().newInstance(); //Object obj = new User()
ResultSetMetaData metaData = resultSet.getMetaData();
for (int i = 0; i < metaData.getColumnCount(); i++) {
String columnName = metaData.getColumnName(i + 1);
String setMethod = "set" + columnName.toUpperCase().charAt(0)
+ columnName.substring(1);
clazz.getMethod(setMethod,String.class).invoke(obj,resultSet.getString(columnName));
}
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} catch (NoSuchMethodException | InstantiationException
| IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
return obj;
}
更好的类型转换方法:
@ParameterizedTest
@CsvSource("32,4")
public void work4(Integer age,Integer id) throws SQLException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//SPI
ResourceBundle bundle = ResourceBundle.getBundle("db");
String url = bundle.getString("jdbc.url");
String username = bundle.getString("jdbc.username");
String pwd = bundle.getString("jdbc.password");
Connection connection = DriverManager.getConnection(url, username, pwd);
String sql = "select id, username, password, name, age from user where age >= ? and id <= ?";
PreparedStatement prep = connection.prepareStatement(sql);
prep.setInt(1,age);
prep.setInt(2,id);
ResultSet resultSet = prep.executeQuery();
//Mock MyBatis resultType
Class<User> resultType = User.class;
while (resultSet.next()){
User user = resultType.getConstructor().newInstance();
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnName(i);
String methodName = "set" + columnName.toUpperCase().charAt(0) + columnName.substring(1);
Field field = resultType.getDeclaredField(columnName);
field.setAccessible(true);
Class<?> fieldType = field.getType();
String fieldTypeSimpleName = fieldType.getSimpleName();
Method method = resultType.getDeclaredMethod(methodName, fieldType);
String getMethodName = null;
switch (fieldTypeSimpleName){
case "Integer" -> getMethodName = "getInt";
case "String" -> getMethodName = "getString";
}
Method resultSetMethod = resultSet.getClass().getDeclaredMethod(getMethodName,String.class);
Object invoke = resultSetMethod.invoke(resultSet,columnName);
method.invoke(user,invoke);
}
System.out.println(user);
}
if (resultSet != null){
resultSet.close();
}
if (connection != null) {
connection.close();
}
}
Web应用Mybatis
转账业务的Service方法:
@Override
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
//判断余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money){
throw new MoneyNotEnoughException("余额不足");
}
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.updateByActno(fromAct);
//空指针异常,转账的金额就会丢失
count += accountDao.updateByActno(toAct);
if (count != 2){
throw new TransferException("转账失败");
}
}
事务应该在这个业务方法的开始开启,执行结束再关闭,也就是事务的控制不能放在DAO的实现类中
但是要保证Service和DAO层用的是同一个SqlSession对象,解决办法:
- 传参
- ThreadLocal
SQLSessionUtil中:
这样在DaoImpl中:
在获取sqlSession对象时,获取到的都是同一个SqlSession对象,但是select方法不能关闭SqlSession对象,如果关闭下面的update就会报错:
在关闭的时候也要进行处理:
也可以接收SqlSession类型参数进行关闭,remove方法不是清空线程池,是移除当前线程的sqlSession对象
Tomcat中线程池的核心线程不会销毁,关闭连接后如果不移除下一次获得的还是关闭的连接
DAO中不能提交,也不能关闭,事务的控制全部在Service中完成
实际上,此处不需要进行open也可与完成事务的控制
但是在service中控制事务还是太突兀了,可以使用动态代理机制:
public AccountService getProxy() throws Exception {
AccountService accountServiceImpl = new AccountServiceImpl();
Class<?> proxyClass = Proxy.getProxyClass(accountServiceImpl.getClass().getClassLoader(), accountServiceImpl.getClass().getInterfaces());
return (AccountService) proxyClass.getConstructor(InvocationHandler.class).newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = SQLSessionUtil.openSession();
Object result = method.invoke(accountServiceImpl, args);
SQLSessionUtil.close(sqlSession);
return result;
}
});
}
作用域和生命周期
SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例(除非连接另一个数据库)。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
SqlSession
每个线程都应该有它自己的 SqlSession 实例(ThreadLocal)。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:
try (SqlSession session = sqlSessionFactory.openSession()) {
// 你的应用逻辑代码
}
当前的Dao层实现类:
DaoImpl中的方法体第一行都是获得sqlSession对象,其余的都是简单的代码,这个类没有存在的必要,可以使用javassist直接在内存中生成Dao的实现类
javassist
Javassist是一个开源的分析、编辑、创建Java字节码的类库,已经加入了JBOSS应用服务器项目,通过使用javassist对字节码操作为JBoss实现动态AOP框架
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
public void testGenerationFirstClass() throws CannotCompileException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//获取类池:用来生成Class
ClassPool pool = ClassPool.getDefault();
//制造类
CtClass ctClass = pool.makeClass("com.eun.bank.dao.impl.AccountDaoImpl");
String methodCode = "public void insert(){System.out.println(123);}";
//制造方法
//参数一:方法代码
//参数二:将要放在哪个类中
CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
//将方法添加到类中
ctClass.addMethod(ctMethod);
//在内存中生成class
ctClass.toClass();
//类加载,返回字节码
Class<?> clazz = Class.forName("com.eun.bank.dao.impl.AccountDaoImpl");
Object daoImpl = clazz.getConstructor().newInstance();
Method insert = clazz.getMethod("insert");
insert.invoke(daoImpl);
}
直接运行会报错:
java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @2328c243
如果是低版本JDK(JDK1.8以下)就没有问题,高版本JDK需要配置两个参数:
-
--add-opens java.base/java.lang=ALL-UNNAMED
-
--add-opens java.base/sun.net.util=ALL-UNNAMED
-
生成类,类实现Dao接口
public void testGenerateImpl() throws CannotCompileException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("com.eun.bank.dao.impl.AccountDaoImpl");
//制造接口
CtClass ctInterface = pool.makeClass("com.eun.bank.dao.impl.AccountDao");
//接口添加到类中 类实现接口
ctClass.addInterface(ctInterface);
String methodCode = "public void delete(){System.out.println(\"hello javassist\");}";
//实现接口中的方法(制造方法)
CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
//方法添加到类中
ctClass.addMethod(ctMethod);
//在内存中生成类,同时将生成的类加载到JVM当中
Class<?> clazz = ctClass.toClass();
AccountDao accountDao = (AccountDao) clazz.getConstructor().newInstance();
accountDao.delete();
}
16行:生成的类是AccountDao类型的子类,就可以使用多态调用,没必要再反射获取方法
10行:目前已知AccountDao接口有且仅有一个delete方法,只需要实现一个,但是实际开发中并不知道接口中有多少方法,方法的返回值、名称、参数列表也是不知道的,动态生成:
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("com.eun.bank.dao.AccountDaoImpl");
CtClass ctInterface = pool.makeClass("com.eun.bank.dao.AccountDao");
ctClass.addInterface(ctInterface);
//制造方法
//问题:制造几个方法?修饰符、方法名、参数列表?
Class<?> aClass = Class.forName("com.eun.bank.dao.AccountDao");
Method[] declaredMethods = aClass.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
StringBuilder builder = new StringBuilder();
//String modifier = Modifier.toString(declaredMethod.getModifiers());
//上面这种做法是不行的,因为接口中的方法是public abstract
//此时要生成的是DaoImpl中的方法,Impl中不能有abstract方法
String modifier = "public ";
String returnType = declaredMethod.getReturnType().getName();
String methodName = declaredMethod.getName();
builder.append(modifier).append(" ")
.append(returnType).append(" ")
.append(methodName).append("(");
Parameter[] parameters = declaredMethod.getParameters();
for (int i = 0; i < parameters.length; i++) {
String paramType = parameters[i].getType().getName();
//参数列表的参数类型要使用getType获取
builder.append(paramType + " " + "arg" + i + ",");
if (i == parameters.length - 1) {
builder.deleteCharAt(builder.length() - 1);
}
}
builder.append(")");
builder.append("{");
/** code */
String methodCode = "System.out.println(\" method execute \");";
builder.append(methodCode);
builder.append("}");
System.out.println(builder.toString());
CtMethod ctMethod = CtMethod.make(builder.toString(), ctClass);
ctClass.addMethod(ctMethod);
}
//调用
Class<?> clazz = ctClass.toClass();
AccountDao accountDao = (AccountDao) clazz.getConstructor().newInstance();
accountDao.delete();
accountDao.selectByActno("");
accountDao.update("",null);
带有返回值的方法的返回值语句还需生成
@Test
public void testGenerationFirstClass() throws CannotCompileException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("com.eun.bank.dao.AccountDaoImpl");
CtClass ctInterface = pool.makeClass("com.eun.bank.dao.AccountDao");
ctClass.addInterface(ctInterface);
//制造方法
//问题:制造几个方法?修饰符、方法名、参数列表?
//需要先获取接口中所有的方法
Method[] methods = AccountDao.class.getDeclaredMethods();
System.out.println(methods.length);
Arrays.stream(methods).forEach(method -> {
//method是接口中的抽象方法,需要将抽象方法实现了
try {
StringBuilder methodCode = new StringBuilder();
methodCode.append("public ");
methodCode.append(method.getReturnType().getName() + " ");
methodCode.append(method.getName() + " (");
Class<?>[] parameterTypes = method.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
methodCode.append(parameterTypes[i].getName() + " arg" + i);
if (i != parameterTypes.length - 1){
methodCode.append(",");
}
}
methodCode.append("){System.out.println(\"hello\");");
String returnTypeSimpleName = method.getReturnType().getSimpleName();
switch (returnTypeSimpleName){
case "void" -> {}
case "int" -> methodCode.append("return 1;");
case "String" -> methodCode.append("return \" hello \";");
}
methodCode.append("}");
System.out.println(methodCode);
CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
ctClass.addMethod(ctMethod);
} catch (Exception e) {
e.printStackTrace();
}
});
Class<?> clazz = ctClass.toClass();
AccountDao accountDao = (AccountDao) clazz.getConstructor().newInstance();
accountDao.delete();
accountDao.selectByActno("");
accountDao.update("",null);
}
在之前的转账业务中,取消AccountDaoImpl实现类,在utils包下提供工具类,生成Dao的实现类
public class GenerateDaoProxy {
/**
* 生成Dao接口的实现类,并返回Dao实现类对象
* @param daoInterface
* @return
*/
public static Object generate(Class daoInterface) throws Exception {
ClassPool pool = ClassPool.getDefault();
//com.powernode.bank.dao.AccountDao -> com.powernode.bank.dao(.impl).AccountDaoProxy
CtClass ctClass = pool.makeClass(daoInterface + "Proxy");
CtClass ctInterface = pool.makeInterface(daoInterface.getName());
ctClass.addInterface(ctInterface);
//实现所有方法
Arrays.stream(daoInterface.getDeclaredMethods()).forEach(method -> {
try {
StringBuilder methodCode = new StringBuilder();
methodCode.append(Modifier.toString(method.getModifiers()) + " ")
.append(method.getReturnType() + " ")
.append(method.getName() + " (");
Class<?>[] parameterTypes = method.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
methodCode.append(parameterTypes[i].getName() + " argv" + i);
if (i != parameterTypes.length - 1){
methodCode.append(",");
}
}
methodCode.append("){");
//方法体中一定是全限定类名
methodCode.append("org.apache.ibatis.session.SqlSession sqlSession = com.eun.bank.util.SQLSessionUtil.openSession();");
//接下来通过sqlSession执行哪个语句?
methodCode.append("}");
System.out.println(methodCode);
CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
ctClass.addMethod(ctMethod);
} catch (Exception e) {
e.printStackTrace();
}
});
Class<?> clazz = ctClass.toClass();
return clazz.getConstructor().newInstance();
}
}
在方法体中,第一行代码一定是:
org.apache.ibatis.session.SqlSession sqlSession= com.eun.bank.util.SQLSessionUtil.openSession();
接下来拼接的就是通过sqlSession对象执行的各个操作,如何确定接下来要拼接的是select、update、delete、insert?从目前来看是无法解决的,因为此时可以获取的信息是:接口中方法的方法名、参数列表、返回值。这些信息无法得知要具体在方法体内执行哪个sql语句,所以Mybatis框架规定:接口中的方法名必须是SQLId的名称,这样通过方法名就可以知道要执行哪个sql语句
SqlCommandType sct= sqlSession.getConfiguration().getMappedStatement(sql的id).getSqlCommandType();
也就是generate
方法还需要一个sqlSession参数,但是sql语句的id是不知道的,所以Mybatis框架规定:
使用GenerateDaoProxy机制,namespace必须是Dao接口的全限定名称,SQLid必须是Dao接口中的方法名
SqlCommandType sct = sqlSession.getConfiguration()
.getMappedStatement(daoInterface.getName() + "." + methodName).getSqlCommandType();
因为sql语句在MappedStatements中的存储方式是:key = 命名空间.sqlId
,所以此处限制命名空间必须是接口名
/**
* 动态生成Dao的实现代理类
*/
public class GenerateDaoProxy {
/**
* 生成Dao接口的实现类,并返回Dao实现类对象
* @param daoInterface
* @return
*/
public static Object generate(SqlSession sqlSession,Class daoInterface) {
System.out.println("here");
System.out.println(daoInterface.getName());
ClassPool pool = ClassPool.getDefault();
//com.powernode.bank.dao.AccountDao -> com.powernode.bank.dao(.impl).AccountDaoProxy
CtClass ctClass = pool.makeClass(daoInterface + "Proxy");
CtClass ctInterface = pool.makeInterface(daoInterface.getName());
ctClass.addInterface(ctInterface);
//实现所有方法
Arrays.stream(daoInterface.getDeclaredMethods()).forEach(method -> {
try {
StringBuilder methodCode = new StringBuilder();
methodCode.append("public ") //此时要生成DaoImpl,不能直接用Modifies.toString 会有abstract
.append(method.getReturnType().getName() + " ")//返回值类型需要getName
.append(method.getName() + " (");
Class<?>[] parameterTypes = method.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
methodCode.append(parameterTypes[i].getName() + " argv" + i);
if (i != parameterTypes.length - 1){
methodCode.append(",");
}
}
methodCode.append("){");
//方法体中一定是全限定类名
methodCode.append("org.apache.ibatis.session.SqlSession sqlSession = com.eun.bank.util.SQLSessionUtil.openSession();");
//注意是dao接口的名称
String sqlId = daoInterface.getName() + "." + method.getName();
SqlCommandType sqlCommandType = sqlSession.getConfiguration()
.getMappedStatement(sqlId).getSqlCommandType();
switch (sqlCommandType){
case SELECT ->
methodCode.append("return sqlSession.selectOne(\"" + sqlId + "\",argv0);");
case UPDATE ->
methodCode.append("return sqlSession.update(\"" + sqlId + "\",argv0);");
case INSERT ->
methodCode.append("return sqlSession.insert(\"" + sqlId + "\",argv0);");
}
methodCode.append("}");
System.out.println(methodCode);
CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
ctClass.addMethod(ctMethod);
} catch (Exception e) {
e.printStackTrace();
}
});
Class<?> clazz = null;
Object obj = new Object();
try {
clazz = ctClass.toClass();
obj = clazz.getConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
但是此时有一个问题:
问题出在javassist生成的DaoProxy类的selectByActno方法上,在生成该方法的返回值类型时:
而拼接方法体时:
必须经过类型转换
GenerateDaoProxy这个类不需要我们写,Mybatis中提供了相关的机制:
private AccountDao accountDao = SQLSessionUtil.openSession().getMapper(AccountDao.class);
实际上是sqlSession的getMapper方法得到映射器
在Tomcat运行这个程序的时候,没有添加两个VM参数,但是运行没有报错,因为:
Tomcat自动添加了这两个运行参数
面向接口进行CRUD
@Test
public void testInsert(){
CarMapper mapper = SQLSessionUtil.openSession().getMapper(CarMapper.class);
mapper.insert(new Car(null,"9876","BWMB",10.01,"2021-1-1","Auto"));
}
如果transaction的type设置为JDBC,autoCommit默认为false,需要手动提交
手动提交需要sqlSession.commit()
,需要sqlSession对象,这个对象可以通过SQLSessionUtil.openSession()
获取,ThreadLocal中获取的都是同一个sqlSession对象,或者可以在getMapper时将这个对象定义出来
小技巧
#{}和${}
-
{}:先编译sql语句,再给占位符传值,底层是PreparedStatement实现。可以防止sql注入,比较常用。
2023-06-20T07:56:09.121563Z 8 Connect root@localhost on mybatis_db using SSL/TLS
2023-06-20T07:56:09.124534Z 8 Query
2023-06-20T07:56:09.131647Z 8 Query SET character_set_results = NULL
2023-06-20T07:56:09.132048Z 8 Query SET autocommit=1
2023-06-20T07:56:09.139911Z 8 Query SET autocommit=0
2023-06-20T07:56:09.166705Z 8 Query select id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where car_type = '电车'
- ${}:先进行sql语句拼接,然后再编译sql语句,底层是Statement实现。存在sql注入现象。只有在需要进行sql语句关键字拼接的情况下才会用到。
2023-06-20T07:57:38.962676Z 9 Connect root@localhost on mybatis_db using SSL/TLS
2023-06-20T07:57:38.965151Z 9 Query
2023-06-20T07:57:38.972312Z 9 Query SET character_set_results = NULL
2023-06-20T07:57:38.972860Z 9 Query SET autocommit=1
2023-06-20T07:57:38.981107Z 9 Query SET autocommit=0
2023-06-20T07:57:39.009463Z 9 Query select id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where car_type = 电车
这样执行就会报错,因为没有 电车 这一列
解决:car_type = '${carType}' 但是这样做是没有意义的
${}使用场景:asc/desc、拼接表名
- 根据价格进行升序/降序排列
如果使用#{}:
最终的SQL语句是:
17:32:48.764 [main] DEBUG com.eun.mapper.CarMapper.selectAllBySc - ==> Preparing: select id, car_num carNum, brand, guide_price guidePrice, produce_time produceTime, car_type carType from t_car order by guidePrice ?;
再对 ? 进行传值,使用setString()
,结果:
17:32:48.764 [main] DEBUG com.eun.mapper.CarMapper.selectAllBySc - ==> Preparing: select id, car_num carNum, brand, guide_price guidePrice, produce_time produceTime, car_type carType from t_car order by guidePrice 'asc';
应该使用${}:
2023-06-20T09:32:02.220006Z 17 Connect root@localhost on mybatis_db using SSL/TLS
2023-06-20T09:32:02.222315Z 17 Query
2023-06-20T09:32:02.229054Z 17 Query SET character_set_results = NULL
2023-06-20T09:32:02.229476Z 17 Query SET autocommit=1
2023-06-20T09:32:02.236594Z 17 Query SET autocommit=0
2023-06-20T09:32:02.262877Z 17 Query select id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car order by guidePrice asc
- 拼接表名
现实业务中可能会存在分表存储数据的情况(存一张表效率较低,数据量太大),可以将这些数据分表存储,扫描的数据量变小了
t_log 日志表,如果只有一张表,每天都会产生很多信息;解决办法:每天生成一张新表:
t_log_20230619
t_log_20230620
t_log_20230621
查询06.20的日志信息:
<select id="selectAllBySc" resultType="com.eun.pojo.Car">
select
*
from
t_log_${date};
</select>
批量删除
一次删除多条记录
#第一种:or
delete from t_car where id = 1 or id = 2 or id = 3;
#第二种 in
delete from t_car where id in(1,2,3)
这是一种错误的写法,最终编译后:
delete from t_car where id in ('1,2,3')
还是需要使用${}
模糊查询
根据汽车品牌进行模糊查询
select * from t_car where brand like '%BMW%'
-
{}
<select id="selectByBrandLike" resultType="com.eun.pojo.Car">
select id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where brand like #{brand};
</select>
但是在向其中传值的时候需要传入 "%BMW%"
- ${}
<select id="selectByBrandLike" resultType="com.eun.pojo.Car">
select id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where brand like '%${brand}%';
</select>
- concat
<select id="selectByBrandLike" resultType="com.eun.pojo.Car">
select id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where brand like concat('%',#{brand},'%');
</select>
"%"#{brand}"%"
<select id="selectByBrandLike" resultType="com.eun.pojo.Car">
select id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where brand like "%"#{brand}"%";
</select>
09:15:17.339 [main] DEBUG com.eun.mapper.CarMapper.selectByBrandLike - ==> Preparing: select id, car_num carNum, brand, guide_price guidePrice, produce_time produceTime, car_type carType from t_car where brand like "%"?"%";
09:15:17.365 [main] DEBUG com.eun.mapper.CarMapper.selectByBrandLike - ==> Parameters: BMW(String)
09:15:17.400 [main] DEBUG com.eun.mapper.CarMapper.selectByBrandLike - <== Total: 1
占位符?
没有在字符串的引号当中,这样就可以被JDBC识别到
别名机制
可以为全限定类名起一个别名,在mybatis-config中配置:
<configuration>
<properties resource="jdbc.properties" />
<typeAliases>
<!-- 哪个类型 别名-->
<typeAlias type="com.eun.pojo.Car" alias="Car" />
</typeAliases>
<environments default="development">
</environments>
</configuration>
使用的时候就可以:
<select id="selectByCarType" resultType="Car"> <!--使用别名-->
select id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where car_type = '${carType}'
</select>
别名在使用的时候是不区分大小写的
namespace 不能使用别名,必须是接口的全限定名称
在mybatis-config中配置typeAlias时,alias属性是可以省略的,别名的默认值是类的SimpleName:
<typeAlias type="com.eun.pojo.Car"/>
<!--别名是 Car car CAR CAR ...-->
但是pojo包下可能有很多的类,这样就要写很多typeAlias标签,Mybatis进行了简化:
<typeAliases>
<package name="com.eun.pojo" />
</typeAliases>
这就会将com.eun.pojo
下所有的类都起别名,默认是SimpleName
所有的别名都不区分大小写
mapper配置
mybatis-config中的mapper标签
<mapper resource="CarMapper.xml"/> <!--从类根路径下开始查找资源-->
<mapper url="file:///d:/CarMapper.xml"/> <!--绝对路径查找-->
<mapper class=""/> <!--mapper接口的全限定接口名-->
对于class属性,给mapper标签提供的应该是XxxMapper.xml文件的位置,为什么要提供全限定接口名?
使用class属性指定接口名,mybatis会在接口的同级目录下查找XxxMapper.xml配置文件
<mapper class="com.eun.mapper.CarMapper"/>
Mybatis会在com/eun/mapper
目录下查找CarMapper.xml文件,也就是说采用这种方式必须保证CarMapper接口和xml配置文件都在同一个包下
解决办法:
在resources下新建目录,保持和包名相同的结构,在生成target文件时:
就会将resource和java目录下的文件整合在一起
xml文件的名字必须和接口名一致
CarMapper接口 -> CarMapper.xml
如果mapper包下有很多类,就要配置很多mapper标签,Mybatis提供了简单的方式:
<mappers>
<package name="com.eun.mapper"/>
</mappers>
在IDEA的resources目录下新建多重目录的话,必须:
com/eun/mybatis/mapper
不能:
com.eun.mybatis.mapper
使用自动生成主键
前提:主键是自动生成的
背景:一个用户有多个角色
插入一条新的记录之后,自动生成了主键,而这个主键需要在其他表中使用时。
插入一个用户数据的同时需要给该用户分配角色:需要将生成的用户的id插入到角色表的user_id字段上。
第一种方式:可以先插入用户数据,再写一条查询语句获取id,然后再插入user_id字段。【比较麻烦】
第二种方式:mybatis提供了一种方式更加便捷。
<insert id="insertCarUseGenerateKey" useGeneratedKeys="true" keyProperty="id">
insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType});
</insert>
- useGeneratedKeys:是否使用生成的主键值
- keyProperty:将主键值保存在对象的哪个属性上,还是调用set方法
CarMapper mapper = SQLSessionUtil.openSession().getMapper(CarMapper.class);
Car car = new Car();
mapper.insertCarUseGenerateKey(car);
SQLSessionUtil.openSession().commit();
System.out.println(car.getId()); //获取自动生成的主键值
Mybatis参数处理
简单类型参数
- byte short int long float double char
- Byte Short Integer Long Float Double Character
- String
- java.util.Date
- java.sql.Date
项目结构
是否可以根据StudentMapper.xml中的配置信息生成StudentMapper接口中的内容?
- Long类型:
@Test
public void selectByIdTest() {
StudentMapper mapper = SQLSessionUtil.openSession().getMapper(StudentMapper.class);
List<Student> students = mapper.selectById(1L);
students.forEach(System.out::println);
}
将 1L 传入后,就会赋值给占位符 #{id}:
<select id="selectById" resultType="Student">
select * from t_student where id = #{id}
</select>
实际上在select标签中有parameterType属性,该属性指定了参数类型
<select id="selectById" resultType="Student" parameterType="java.lang.Long">
select * from t_student where id = #{id}
</select>
SQL语句最终是这样的:
select * from t_student where id = ?
传值需要:
prep.setLong(1,1L);
prep.setString(1,"1L");
...
调用setXxx方法,取决于paramterType属性
Mybatis可以做到类型的自动推断,所以此处可以省略,指定该类型可以省略自动推断的过程,效率更高
对于Object类型来说,获取到#{}中的内容后,反射调用get方法,可以获取到返回值类型
paramterType可以使用别名,Mybatis内置了很多别名,可以参考开发手册
<select id="selectById" resultType="Student" parameterType="Long">
select * from t_student where id = #{id}
</select>
- String类型
<select id="selectByName" resultType="Student" parameterType="String">
select * from t_student where name = #{name}
</select>
其实完整的映射文件应该这样写:
<select id="selectByName" resultType="student" parameterType="java.lang.String">
select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR}
</select>
其中sql语句中的javaType,jdbcType,以及select标签中的parameterType属性,都是用来帮助mybatis进行类型确定的。不过这些配置多数是可以省略的。因为mybatis它有强大的自动类型推断机制。
- javaType:可以省略
- jdbcType:可以省略
- parameterType:可以省略
加上这些参数效率会更高一些,跳过了自动推断的阶段
- Date类型
<select id="selectByBirth" resultType="Student" parameterType="Date">
select * from t_student where birth = #{birth}
</select>
public void selectByBirthTest() throws ParseException {
StudentMapper mapper = SQLSessionUtil.openSession().getMapper(StudentMapper.class);
Date birth = new SimpleDateFormat("yyyy-MM-dd").parse("1980-10-11");
List<Student> students = mapper.selectByBirth(birth);
students.forEach(System.out::println);
}
最终执行的sql语句:
2023-06-21T11:22:06.262757Z 37 Query SET character_set_results = NULL
2023-06-21T11:22:06.263226Z 37 Query SET autocommit=1
2023-06-21T11:22:06.270877Z 37 Query SET autocommit=0
2023-06-21T11:22:06.318191Z 37 Query select * from t_student where birth = '1980-10-11 00:00:00'
省略parameterType也不会报错,说明Date也是一种简单类型
- Character类型
<select id="selectBySex" resultType="Student" parameterType="java.lang.Character">
select * from t_student where sex = #{sex}
</select>
@Test
public void selectBySexTest() {
StudentMapper mapper = SQLSessionUtil.openSession().getMapper(StudentMapper.class);
Character sex = '男';
List<Student> students = mapper.selectBySex('男');
students.forEach(System.out::println);
}
Map类型参数
<insert id="insertStudentByMap" parameterType="map">
insert into t_student values (null,#{name},#{age},#{height},#{birth},#{gender})
</insert>
@Test
public void insertStudentByMapTest() {
SqlSession sqlSession = SQLSessionUtil.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
HashMap<String, Object> map = new HashMap<>();
map.put("name","赵六");
map.put("age",20);
map.put("height",1.81);
map.put("gender",'男');
map.put("birth",new Date());
studentMapper.insertStudentByMap(map);
sqlSession.commit();
sqlSession.close();
}
pojo类型参数
<insert id="insert" paramterType="Student">
insert into t_student values(null,#{name},#{age},#{height},#{birth},#{sex})
</insert>
@Test
public void testInsert(){
Student student = new Student();
student.setName("李四");
student.setAge(30);
student.setHeight(1.70);
student.setSex('男');
student.setBirth(new Date());
int count = mapper.insert(student);
SqlSessionUtil.openSession().commit();
}
运行正常,数据库中成功添加一条数据。
这里需要注意的是:#{} 里面写的是属性名字。这个属性名其本质上是:set/get方法名去掉set/get之后的名字。
多参数
根据name和gender查询
List<Student> selectByNameAndGender(String name,Character gender);
向SQL语句传值:
<select id="selectByNameAndGender" resultType="Student" >
select * from t_student where name = #{name} and gender = #{gender}
</select>
直接运行报错:
对于多个参数,mybatis框架底层会自动创建一个Map集合,Map集合以如下方式存储元素:
map.put("argv0",name);
map.put("argv1",gender);
map.put("param1",name);
map.put("param2",gender);
需要改为:
<select id="selectByNameAndGender" resultType="Student" >
select * from t_student where name = #{arg0} and gender = #{param2}
</select>
@Param
arg0和param1的可读性太差,可以使用@Param
注解指定名字
List<Student> selectByNameAndGender(@Param("name") String name, @Param("gender") Character gender);
这样mybatis框架底层就会创建map集合,存储元素:
map.put("name",name);
map.put("gender",gender);
map.put("param1",name);
map.put("param2",gender);
源码分析:
studentMapper引用指向了代理对象,selectByNameAndGender是代理方法
- Object proxy :代理对象
- Method method : 目标方法
- Object[] args : 调用方法时传递的参数
进入:
匹配SELECT成功,方法返回值List -> 多个:
进入:
进入:
names集合是一个SortedMap,是一个成员变量:
Object[] args
: 按顺序将参数放入数组
SortedMap<Integer,String> names
: 按顺序存储params的名称
如果注解中给定的名称就是param1,上面的if判断就不会对应的进入判断了
Mybatis查询处理
select查询的语句返回的数据封装为各种对象
查询时如果返回多条记录,但是使用一个pojo类型接收:
/**
* 查询的结果有多个,但是采用一个pojo类型接收
*/
Car selectByBrandLike(String brand);
<select id="selectByBrandLike" resultType="Car">
select
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car
where brand like '%${brand}%';
</select>
如果查询的返回结果只有一条,没有任何问题:
public void selectByBrandLikeTest() {
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = mapper.selectByBrandLike("BYDA");
System.out.println(car);
}
如果查询的返回结果有多条,直接报错:
08:35:25.664 [main] DEBUG com.eun.mapper.CarMapper.selectByBrandLike - ==> Preparing: select id, car_num carNum, brand, guide_price guidePrice, produce_time produceTime, car_type carType from t_car where brand like '%BYD%';
08:35:25.689 [main] DEBUG com.eun.mapper.CarMapper.selectByBrandLike - ==> Parameters:
setId : 18
...
setId : 111
setId : 112
setId : 113
setId : 119
08:35:25.742 [main] DEBUG com.eun.mapper.CarMapper.selectByBrandLike - <== Total: 94
org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 94
#TooManyResultsException 期待selectOne方法的一个返回值,但是返回了94个
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:80)
Process finished with exit code -1
返回Map
如果查询结果没有合适的pojo类对应,没有合适的pojo对象接收查询结果,可以使用Map集合接收结果
对于多表联查就是这种情况
Map<String,Object> selectByIdRetMap(Long id);
<select id="selectByIdRetMap" resultType="Map">
select
*
from t_car
where id = #{id};
</select>
@Test
public void selectByIdRetMapTest() {
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Map<String, Object> map = mapper.selectByIdRetMap(1L);
System.out.println(map);
}
//{car_num=1001, id=1, guide_price=10.00, produce_time=2020-10-11, brand=BMW, car_type=燃油车}
- 返回多个Map
如果返回的结果数 > = 1条记录,可以返回一个存储Map 的 List集合,等同于List
List<Map<String,Object>> selectAllRetListMap();
<select id="selectAllRetListMap" resultType="map">
select * from t_car;
</select>
<!--resultType是List集合中存储的元素-->
但是这样返回的结果,获取某个指定的数据时比较繁琐,加入要获取id = 3 的car的brand信息:
@Test
public void selectAllRetListMapTest() {
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Map<String, Object>> maps = mapper.selectAllRetListMap();
maps.forEach(map -> {
if (Objects.equals(map.get("id"), 3L)){
System.out.println(map.get("brand"));
}
});
}
可以使用大Map集合进行改进:
- 返回
Map<String,Map<String,Object>>
使用Car的id作为key,每条记录对应的Map集合作为value
/**
* 查询所有信息,返回一个Map集合
* Map集合的key是每条记录的id
* Map集合的value是每条记录对应的map信息
* @MapKey("id") : 查询结果的id作为大Map集合的key
*/
@MapKey("id")
Map<Long,Map<String,Object>> selectAllRetMap();
<select id="selectAllRetMap" resultType="map">
select * from t_car;
</select>
@Test
public void selectAllRetMapTest() {
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Map<Long, Map<String, Object>> map = mapper.selectAllRetMap();
System.out.println(map);
//获取id = 3l的记录
Map<String,Object> car = map.get(3L);
}
/**
{
1={car_num=1001, id=1, guide_price=10.00, produce_time=2020-10-11, brand=BMW, car_type=燃油车},
2={car_num=1002, id=2, guide_price=55.00, produce_time=2020-11-11, brand=Benzi, car_type=新能源},
5={car_num=1003, id=5, guide_price=30.00, produce_time=2000-10-11, brand=FT, car_type=燃油车},
6={car_num=1003, id=6, guide_price=30.00, produce_time=2000-10-11, brand=FT, car_type=燃油车},
7={car_num=1003, id=7, guide_price=30.00, produce_time=2000-10-11, brand=AD, car_type=燃油车},
8={car_num=1003, id=8, guide_price=30.00, produce_time=2000-10-11, brand=AD, car_type=燃油车},
9={car_num=1003, id=9, guide_price=30.00, produce_time=2000-10-11, brand=AD, car_type=燃油车},
}
*/
结果映射
查询结果的列名和pojo类的属性名不同的处理方法:
- as起别名
- 使用resultMap进行结果映射
- 配置settings是否开启驼峰命名自动映射
- 使用resultMap进行结果映射:指定数据库表中的字段名和pojo对象的属性名的对应关系
/**
* 查询所有Car信息,使用resultMap标签进行结果映射
*/
List<Car> selectAllByResultMap();
<!--select标签的resultMap属性用来指定使用哪个结果映射-->
<select id="selectAllByResultMap" resultMap="carResultMap">
select * from t_car;
</select>
<!--
定义一个结果映射,在这个结果映射中指定数据库表的字段名和Java类的属性名的对应关系
1. id属性: 指定resultMap的唯一标识
2. type属性: 指定pojo类的类名,可以使用别名
-->
<resultMap id="carResultMap" type="Car">
<!-- 如果数据库表中有主键,建议配一个id标签,可以提高mybatis的执行效率 -->
<id property="id" column="id"/>
<result property="carNum" column="car_num" />
<result property="guidePrice" column="guide_price" />
<result property="produceTime" column="produce_time" />
<result property="carType" column="car_type" javaType="String" jdbcType="VARCHAR"/>
<!--如果property和column是一样的,可以省略-->
</resultMap>
注意:resultMap只在结果的实体类属性比较复杂,包含集合等数据结构时使用的
- 开启驼峰命名自动映射
前提:属性名遵循java命名规范,数据库字段名遵循SQL命名规范
实体类中的属性名 | 数据库表的列名 |
---|---|
carNum | car_num |
carType | car_type |
produceTime | produce_time |
启用该功能:mybatis-config.xml文件中配置:
<!--放在properties标签后面-->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
使用该配置的标签都会指定resultType=car
<select id="selectAllByMapUnderscoreToCamelCase" resultType="Car">
select * from t_car
</select>
@Test
public void testSelectAllByMapUnderscoreToCamelCase(){
CarMapper carMapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
List<Car> cars = carMapper.selectAllByMapUnderscoreToCamelCase();
System.out.println(cars);
}
返回总记录条数
Long selectTotal();
<select id="selectTotal" resultType="Long">
select count(*) from t_car;
</select>
动态SQL
业务场景:
-
批量删除
前端提交的数据:
uri?id=1&id=2&id=3&id=4
获取:
String[] ids = {"1","2","3","4"}
SQL:
delete from t_car where id in(1,2,3,4,5,6,......这里的值是动态的,根据用户选择的id不同,值是不同的);
-
多条件查询:
select * from t_car where brand like '丰田%' and guide_price > 30 and .....;
根据不同的条件提供不同的SQL语句
if标签
需求:多条件查询。
可能的条件包括:品牌(brand)、指导价格(guide_price)、汽车类型(car_type)
List<Car> selectByMultiCondition(
@Param("brand") String brand,
@Param("guidePrice") String guidePrice,
@Param("carType") String carType);
<select id="selectByMultiCondition" resultType="Car">
select * from t_car where
/*
if标签的test属性是必须的,值必须是true或者false
如果为true,if标签的sql语句就会拼接
test属性可以使用的是:
如果Mapper接口使用了@Param注解,test中要出现的是@Param注解指定的参数名
如果没有使用@Param注解,test中应该使用 arg0 arg1 param1 param2
如果使用pojo传参,test中出现的是pojo类的属性名
test属性中 并且 使用 and 表示
*/
<if test="brand != null and brand != ''">
brand like '%${brand}%'
</if>
<if test="guidePrice != null and guidePrice != ''">
and guide_price > #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</select>
如果查询时所有的条件都不为空:
@Test
public void selectByMultiConditionTest() {
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByMultiCondition("BWM", 2.0, "Auto");
cars.forEach(System.out::println);
}
可以查询到对应的记录
如果查询时所有的条件都为空:
List<Car> cars = mapper.selectByMultiCondition(null,null,null);
直接报错:
这样就是多出来了一个where,可以在后面添加一个 1 = 1
<select id="selectByMultiCondition" resultType="Car">
select * from t_car where 1 = 1
<if test="brand != null and brand != ''">
brand like '%${brand}%'
</if>
<if test="guidePrice != null and guidePrice != ''">
and guide_price > #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</select>
这样做在三个条件都不为空时还要加额外的控制,否则还会报错:
解决办法:在第一个if标签中加一个and
<select id="selectByMultiCondition" resultType="Car">
select * from t_car where 1 = 1
<if test="brand != null and brand != ''">
and brand like '%${brand}%'
</if>
<if test="guidePrice != null and guidePrice != ''">
and guide_price > #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</select>
这样在哪个条件为空时都不会报错了
where标签
-
所有条件为空时,where标签保证不会生成where子句;
-
自动去除某些条件前面多余的and或or
<select id="selectByMultiCondition" resultType="Car">
select * from t_car
<where>
<if test="brand != null and brand != ''">
brand like '%${brand}%'
</if>
<if test="guidePrice != null and guidePrice != ''">
and guide_price > #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</where>
</select>
- 都不为空时:
select * from t_car WHERE brand like '%BWM%' and guide_price > ? and car_type = ?
- 都为空时:
select * from t_car
可以不生成where
- 如果第一个条件为空,后面两个条件不为空,而后面两个条件之前都有and:
List<Car> cars = mapper.selectByMultiCondition(null,2.0,"Auto");
select * from t_car WHERE guide_price > ? and car_type = ?
会自动将这个and去掉
- 三个条件前都加and:
<select id="selectByMultiCondition" resultType="Car">
select * from t_car
<where>
<if test="brand != null and brand != ''">
and brand like '%${brand}%'
</if>
<if test="guidePrice != null and guidePrice != ''">
and guide_price > #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</where>
</select>
这样也是不会报错的,会自动将and/or去掉
但是只能去除前面的and/or,不能去除后面的and/or
trim标签
- prefix:在trim标签中的语句前添加内容
- suffix:在trim标签中的语句后添加内容
- prefixOverrides:前缀覆盖掉(去掉)
- suffixOverrides:后缀覆盖掉(去掉)
<select id="selectByMultiCondition" resultType="Car">
select * from t_car
<!--
prefix="where" 在trim标签所有的内容前面添加 where
suffixOverrides="and|or" 把trim标签中内容的后缀 and或or 去掉
-->
<trim prefix="where" suffixOverrides="and|or">
<if test="brand != null and brand != ''">
brand like '%${brand}%' and
</if>
<if test="guidePrice != null and guidePrice != ''">
guide_price > #{guidePrice} and
</if>
<if test="carType != null and carType != ''">
car_type = #{carType}
</if>
</trim>
</select>
如果所有条件都为空,就不会添加prefix前缀
set标签
使用在update语句中,用来:
- 生成关键字set
- 去掉最后多余的
,
场景:只更新提交的不为空的字段,如果提交的数据是空或者 ""
,这个字段就不更新了
<update id="updateById">
update t_car
<set>
<if test="carNum != null and carNum != ''">car_num = #{carNum},</if>
<if test="brand != null and brand != ''">brand = #{brand},</if>
<if test="guidePrice != null and guidePrice != ''">guide_price = #{guidePrice},</if>
<if test="produceTime != null and produceTime != ''">produce_time = #{produceTime},</if>
<if test="carType != null and carType != ''">car_type = #{carType},</if>
</set>
where
id = #{id};
</update>
choose when otherwise标签
<choose>
<when test=""></when>
<when test=""></when>
<otherwise></otherwise>
</choose>
只有一个分支会被选择
业务:先根据品牌查询,再根据指导价查询,最后根据汽车类型查询
List<Car> selectByChoose(
@Param("brand") String brand,
@Param("guidePrice") Double guidePrice,
@Param("carType") String carType);
<select id="selectByChoose" resultType="Car">
select * from t_car
<where>
<choose>
<when test="brand != null and brand != ''">
brand like '%${brand}%'
</when>
<when test="guidePrice != null and guidePrice != ''">
guide_price = #{guidePrice}
</when>
<otherwise>
car_type = #{carType}
</otherwise>
</choose>
</where>
</select>
如果brand不为空,就按brand查询,如果brand为空,就按guidePrice查询,最终只有一个分支执行
foreach批量删除
int deleteByIds(Long[] ids);
<!--
foreach标签的属性:
collection:指定数组或者集合
item : 代表数组或者集合中的元素
separator : 循环之间的分隔符
-->
<delete id="deleteByIds">
delete from t_car where id in (
<foreach collection="ids" item="id" separator="," >
#{id}
</foreach>
)
</delete>
不要手动添加 , separator可以去除最后的 ,
@Test
public void deleteByIdsTest() {
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
mapper.deleteByIds(new Long[]{1l,2l,5l,6l});
sqlSession.commit();
sqlSession.close();
}
### Error updating database. Cause: org.apache.ibatis.binding.BindingException: Parameter 'ids' not found. Available parameters are [array, arg0]
### The error may exist in com/eun/mapper/CarMapper.xml
### The error may involve com.eun.mapper.CarMapper.deleteByIds
### The error occurred while executing an update
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'ids' not found. Available parameters are [array, arg0]
需要使用Param注解指定名称
int deleteByIds(@Param("ids") Long[] ids);
其他写法:
<delete id="deleteByIds">
delete from t_car where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
使用or删除:
delete from t_user where id = 1 or id = 2 or id = 3
<delete id="deleteByIds">
delete from t_car where
<foreach collection="ids" item="id" separator="or">
id = #{id}
</foreach>
</delete>
2023-06-23T06:24:42.723502Z 91 Query delete from t_car where
id = 1
or
id = 2
or
id = 5
or
id = 6
2023-06-23T06:24:42.724647Z 91 Query commit
2023-06-23T06:24:42.725263Z 91 Query SET autocommit=1
foreach批量插入
insert into t_user (id,name,age)
values
('002','zs',20),
('003','zs',20),
('004','zs',20)
<insert id="insertUsers">
insert into t_user
values
<foreach collection="users" item="user" separator=",">
(#{user.id},#{user.name},#{user.age})
</foreach>
</insert>
接口:
int insertUsers(@Param("users") User[] users);
@Test
public void insertUsersTest() {
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
User[] users = {
new User("002","zs",20),
new User("003","zs",20),
new User("004","zs",20)
};
mapper.insertUsers(users);
sqlSession.commit();
}
sql:
2023-06-23T06:19:05.036796Z 89 Query insert into t_user
values
('002','zs',20),
('003','zs',20),
('004','zs',20)
2023-06-23T06:19:05.049481Z 89 Query commit
sql标签和include标签
sql标签用来声明sql片段,include标签用来将声明的sql片段包含到某个sql语句当中
作用:代码复用,易维护
<sql id="carCols">
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
</sql>
使用:
<select id="selectAllRetMap" resultType="map">
select <include refid="carCols"/> from t_car
</select>
高级映射
之前的映射都是单表的映射,现实开发中数据可能存在多张表中
准备数据库:一个班级对应多个学生,班级表:t_clazz,学生表:t_student
主表原则:谁在前谁是主表
多对一:多在前,多是主表
一对多:一在前,一是主表
多对一映射
多个学生对应一个班级
此时的主表是学生表t_stu,JVM中的主对象是Student对象,副对象是Clazz对象
如果使用Map返回数据,得到的结果应该是:
key | value |
---|---|
sid | |
sname | |
cid | |
cname |
如果此时的业务是展示学生的所有信息,就是多个学生对应一个班级
但是返回的结果应该是pojo对象,根据学生表的cid属性可以找到对应的Clazz对象,关联关系:
classDiagram direction LR class Student{ - Integer sid - String sname - Clazz clazz } class Clazz{ - Integer cid - String cname } Student --> Clazz通过学生对象就可以找到班级对象
Mybatis有三种方式进行多对一映射:
- 一条SQL语句,级联属性映射
- 一条SQL语句,association
- 两条SQL语句,分步查询(可复用、延迟加载)
级联属性映射
<resultMap id="studentRetMap" type="Student">
<id property="sid" column="sid" />
<result property="clazz.cid" column="cid" />
<result property="clazz.cname" column="cname" />
</resultMap>
<select id="selectById" resultMap="studentRetMap">
select
s.sid,s.sname,c.cid,c.cname
from
t_stu s
left join
t_clazz c
on
s.cid = c.cid
where s.sid = #{sid}
</select>
+-----+-------+------+----------+
| sid | sname | cid | cname |
+-----+-------+------+----------+
| 1 | 张三 | 1000 | 高三一班 |
+-----+-------+------+----------+
association
<resultMap id="studentRetMapAssociation" type="Student">
<id property="sid" column="sid" />
<result property="sname" column="sname" /> <!--必须-->
<!--
association 关联: 一个Student对象,关联一个Clazz对象
property:要映射的pojo类的属性名
javaType:要映射的POJO类名
-->
<association property="clazz" javaType="Clazz">
<id property="cid" column="cid" />
<result property="cname" column="cname" />
</association>
</resultMap>
<select id="selectByIdAssociation" resultMap="studentRetMapAssociation">
select
s.sid,s.sname,c.cid,c.cname
from
t_stu s left join t_clazz c on s.cid = c.cid
where s.sid = #{sid}
</select>
这种方式在定义结果映射时即使property和column是相同的也不能省略
分步查询
首先根据sid查询Student信息,获得cid后再查询Clazz信息:
<resultMap id="studentRetMapByStep" type="Student">
<id property="sid" column="sid" />
<!--
association : 指定第二步查询的sql的id
第二步查询的结果赋值给property属性指定的属性名
第二步查询是根据cid查询班级信息,应该定义在ClazzMapper.xml中
并且应该将第一步的查询结果中的cid通过column属性传递过去
-->
<association property="clazz"
select="com.eun.mapper.ClazzMapper.selectByIdStep2"
column="cid" />
</resultMap>
<select id="selectByIdStep1" resultMap="studentRetMapByStep">
select * from t_stu where sid = #{sid}
</select>
<mapper namespace="com.eun.mapper.ClazzMapper">
<select id="selectByIdStep2" resultType="Clazz">
select * from t_clazz where cid = #{cid};
</select>
</mapper>
测试:
@Test
public void selectByIdStep1Test() {
SqlSession sqlSession = SQLSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.selectByIdStep1(1);
System.out.println(student);
}
优点:
-
可复用:根据cid查询班级的操作可以复用
-
延迟加载:用到的时候再执行查询语句,不用的时候不查询
表连接的次数是笛卡儿积的结果,关联的次数越多匹配的次数越多,效率就越低,延迟加载可以避免
mybatis中开启延迟加载:
association标签添加fetchType="lazy"
用到的时候再查询,此时想演示不能直接输出Student,可以输出Student的sname:
@Test
public void selectByIdStep1Test() {
SqlSession sqlSession = SQLSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.selectByIdStep1(1);
System.out.println(student.getSname());
}
只执行了一条sql语句,默认情况下没有开启延迟加载
在association中设置fetchType只针对当前关联的sql语句有效,全局:
实际开发中大部分都要使用延迟加载,建议全部开启延迟加载机制
如果其中的某一个SQL不想使用延迟加载,使用fetchType = “eager”设置assocation就可以了
一对多映射
业务:展示班级中所有的学生信息
一是主表,如何表示一个班级对应的多个学生对象?
通常在一方中添加List集合
classDiagram direction LR class Student{ - Integer sid - String sname } class Clazz{ - Integer cid - String cname - List~Student~ students } Clazz --> Student根据班级编号从学生表中查出所有的学生
两种实现方式:
- collection标签
- 分步查询
collection
<resultMap id="clazzResultMap" type="Clazz">
<id property="cid" column="cid" />
<result property="cname" column="cname" />
<!-- ofType 指定集合中元素的类型 -->
<collection property="students" ofType="Student">
<id property="sid" column="sid" />
<result property="sname" column="sname"/>
</collection>
</resultMap>
<select id="selectByCollection" resultMap="clazzResultMap">
select c.cid,c.cname,s.sid,s.sname from t_clazz c left join t_stu s on c.cid = s.cid where c.cid = #{cid}
</select>
一条SQL语句,多表连接查询
分步查询
<resultMap id="clazzResultMapAssociation" type="Clazz">
<id property="cid" column="sid"/>
<result property="cname" column="cname"/>
<!--用association和collection的结果是一样的-->
<association property="students"
select="com.eun.mapper.StudentMapper.selectByCid"
column="cid"/>
</resultMap>
<select id="selectByAssociation" resultMap="clazzResultMapAssociation">
select * from t_clazz where cid = #{cid}
</select>
<select id="selectByCid" resultType="Student">
select * from t_stu where cid = #{cid}
</select>
Mybatis缓存
缓存:cache
缓存的作用:通过减少IO的方式,来提高程序的执行效率。
mybatis的缓存:将select语句的查询结果放到缓存(内存)当中,下一次还是这条select语句的话,直接从缓存中取,不再查数据库。一方面是减少了IO。另一方面不再执行繁琐的查找算法。效率大大提升。
mybatis缓存包括:
- 一级缓存:将查询到的数据存储到
SqlSession
中。 - 二级缓存:将查询到的数据存储到
SqlSessionFactory
中。 - 或者集成其它第三方的缓存:比如EhCache【Java语言开发的】、Memcache【C语言开发的】等。
缓存只针对于DQL语句,也就是说缓存机制只对应select语句。
一级缓存:数据在一次会话中缓存
在一次会话中进行两次查询,mysql日志:
这时的数据是放在sqlSession对象中的,只进行了一次查询,说明第二次查询时从一级缓存中获取数据
如果关闭sqlSession对象再重新获取,就会绕开一级缓存:
在一次数据库会话中进行了两次查询操作,说明二级缓存默认是没有开启的
- 什么时候不走缓存?
- SqlSession对象不是同一个
- 查询条件不同
- 什么时候一级缓存失效?
- 执行了sqlSession的clearCache方法,手动清空缓存
- 执行了insert、delete、update(不管对哪张表操作)
都会清空一级缓存
二级缓存
二级缓存的范围是SqlSessionFactory,数据库级别的缓存
条件:
-
<setting name="cacheEnabled" value="true">
全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认就是true,无需设置。 -
在需要使用二级缓存的SqlMapper.xml文件中添加配置:
<cache />
-
使用二级缓存的实体类对象必须是可序列化的,也就是必须实现java.io.Serializable接口
-
SqlSession对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存当中。此时二级缓存才可用。
应该是不会走二级缓存的,因为sqlSession1对象并没有提交或关闭,数据没有提交到二级缓存当中
2023-06-23T12:07:13.207620Z 124 Connect root@localhost on mybatis_db using SSL/TLS
2023-06-23T12:07:13.229027Z 124 Query SET autocommit=1
2023-06-23T12:07:13.237472Z 124 Query SET autocommit=0
2023-06-23T12:07:13.266410Z 124 Query select
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where id = 1
2023-06-23T12:07:13.313035Z 125 Connect root@localhost on mybatis_db using SSL/TLS
2023-06-23T12:07:13.314491Z 125 Query SET character_set_results = NULL
2023-06-23T12:07:13.314806Z 125 Query SET autocommit=1
2023-06-23T12:07:13.315437Z 125 Query SET autocommit=0
2023-06-23T12:07:13.316507Z 125 Query select
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where id = 1
2023-06-23T12:07:13.320493Z 124 Query SET autocommit=1
2023-06-23T12:07:13.321545Z 125 Query SET autocommit=1
此时的缓存命中率:
两次查询都是0.0
关闭sqlSession1:
2023-06-23T12:08:11.254088Z 126 Query SET character_set_results = NULL
2023-06-23T12:08:11.254592Z 126 Query SET autocommit=1
2023-06-23T12:08:11.262306Z 126 Query SET autocommit=0
2023-06-23T12:08:11.291085Z 126 Query select
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where id = 1
2023-06-23T12:08:11.324926Z 126 Query SET autocommit=1
此时的缓存命中率:
第一次缓存命中率0.0,第二次缓存命中率0.5
程序执行到sqlSession.close()
,都会将数据从一级缓存放在二级缓存当中
一级缓存的优先级比二级缓存高,从二级缓存中取的前提是一级缓存已经关闭了
二级缓存失效:两次查询之间出现了增删改操作(一级缓存同时失效)
二级缓存相关配置:
- eviction:指定从缓存中移除某个对象的淘汰算法。默认采用LRU策略。
- LRU:Least Recently Used。最近最少使用。优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用。)
- FIFO:First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰。
- SOFT:软引用。淘汰软引用指向的对象。具体算法和JVM的垃圾回收算法有关。
- WEAK:弱引用。淘汰弱引用指向的对象。具体算法和JVM的垃圾回收算法有关。
- flushInterval:二级缓存的刷新时间间隔。单位毫秒。如果没有设置。就代表不刷新缓存,只要内存足够大,一直会向二级缓存中缓存数据。除非执行了增删改。
- readOnly:
- true:多条相同的sql语句执行之后返回的对象是共享的同一个。性能好。但是多线程并发可能会存在安全问题。
- false:多条相同的sql语句执行之后返回的对象是副本,调用了clone方法。性能一般。但安全。
- size:设置二级缓存中最多可存储的java对象数量。默认值1024。
集成EhCache
集成EhCache是为了代替mybatis自带的二级缓存。一级缓存是无法替代的。
mybatis对外提供了接口,也可以集成第三方的缓存组件。比如EhCache、Memcache等。都可以。
EhCache是Java写的。Memcache是C语言写的。所以mybatis集成EhCache较为常见,按照以下步骤操作,就可以完成集成:
第一步:引入mybatis整合ehcache的依赖。
<!--mybatis集成ehcache的组件-->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
第二步:在类的根路径下新建echcache.xml文件,并提供以下配置信息。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--磁盘存储:将缓存中暂时不使用的对象,转移到硬盘,类似于Windows系统的虚拟内存-->
<diskStore path="e:/ehcache"/>
<!--defaultCache:默认的管理策略-->
<!--eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断-->
<!--maxElementsInMemory:在内存中缓存的element的最大数目-->
<!--overflowToDisk:如果内存中数据超过内存限制,是否要缓存到磁盘上-->
<!--diskPersistent:是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false-->
<!--timeToIdleSeconds:对象空闲时间(单位:秒),指对象在多长时间没有被访问就会失效。只对eternal为false的有效。默认值0,表示一直可以访问-->
<!--timeToLiveSeconds:对象存活时间(单位:秒),指对象从创建到失效所需要的时间。只对eternal为false的有效。默认值0,表示一直可以访问-->
<!--memoryStoreEvictionPolicy:缓存的3 种清空策略-->
<!--FIFO:first in first out (先进先出)-->
<!--LFU:Less Frequently Used (最少使用).意思是一直以来最少被使用的。缓存的元素有一个hit 属性,hit 值最小的将会被清出缓存-->
<!--LRU:Least Recently Used(最近最少使用). (ehcache 默认值).缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存-->
<defaultCache eternal="false" maxElementsInMemory="1000" overflowToDisk="false"
diskPersistent="false" timeToIdleSeconds="0"
timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU"/>
</ehcache>
第三步:修改SqlMapper.xml文件中的
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
第四步:编写测试程序使用。
@Test
public void selectByIdTest() throws IOException {
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
mapper1.selectById(1l);
sqlSession1.close();
mapper2.selectById(1l);
sqlSession2.close();
}
20:27:51.259 [main] DEBUG com.eun.mapper.CarMapper.selectById - ==> Preparing: select id, car_num carNum, brand, guide_price guidePrice, produce_time produceTime, car_type carType from t_car where id = ?;
20:27:51.283 [main] DEBUG com.eun.mapper.CarMapper.selectById - ==> Parameters: 1(Long)
20:27:51.310 [main] DEBUG com.eun.mapper.CarMapper.selectById - <== Total: 0
20:27:51.312 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@aa22f1c]
20:27:51.313 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@aa22f1c]
20:27:51.313 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 178401052 to pool.
20:27:51.314 [main] DEBUG com.eun.mapper.CarMapper - Cache Hit Ratio [com.eun.mapper.CarMapper]: 0.5
Process finished with exit code 0
Mybatis逆向工程
根据数据库表逆向生成Java的pojo类、SqlMapper.xml、Mapper接口等
- pom中添加逆向工程插件
<!--定制构建过程-->
<build>
<!--可配置多个插件-->
<plugins>
<!--其中的一个插件:mybatis逆向工程插件-->
<plugin>
<!--插件的GAV坐标-->
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.1</version>
<!--允许覆盖 将上一次生成的内容清空再写入-->
<configuration>
<overwrite>true</overwrite>
</configuration>
<!--插件的依赖-->
<dependencies>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
-
配置generatorConfig.xml,该文件名必须叫做:generatorConfig.xml,该文件必须放在类的根路径下。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- targetRuntime有两个值: MyBatis3Simple:生成的是基础版,只有基本的增删改查。 MyBatis3:生成的是增强版,除了基本的增删改查之外还有复杂的增删改查。 --> <context id="DB2Tables" targetRuntime="MyBatis3"> <!--防止生成重复代码--> <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/> <commentGenerator> <!--是否去掉生成日期--> <property name="suppressDate" value="true"/> <!--是否去除注释--> <property name="suppressAllComments" value="true"/> </commentGenerator> <!--连接数据库信息--> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/powernode" userId="root" password="root"> </jdbcConnection> <!-- 生成pojo包名和位置 --> <javaModelGenerator targetPackage="com.powernode.mybatis.pojo" targetProject="src/main/java"> <!--是否开启子包--> <property name="enableSubPackages" value="true"/> <!--是否去除字段名的前后空白--> <property name="trimStrings" value="true"/> </javaModelGenerator> <!-- 生成SQL映射文件的包名和位置 --> <sqlMapGenerator targetPackage="com.powernode.mybatis.mapper" targetProject="src/main/resources"> <!--是否开启子包--> <property name="enableSubPackages" value="true"/> </sqlMapGenerator> <!-- 生成Mapper接口的包名和位置 --> <javaClientGenerator type="xmlMapper" targetPackage="com.powernode.mybatis.mapper" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> </javaClientGenerator> <!-- 表名和对应的实体类名--> <table tableName="t_car" domainObjectName="Car"/> </context> </generatorConfiguration>
- 运行mybatis-generator:generator
基础版:
public interface CarMapper {
int deleteByPrimaryKey(Long id);
int insert(Car row);
Car selectByPrimaryKey(Long id);
List<Car> selectAll();
int updateByPrimaryKey(Car row);
}
增强版:
public interface CarMapper {
long countByExample(CarExample example);
int deleteByExample(CarExample example);
int deleteByPrimaryKey(Long id);
int insert(Car row);
int insertSelective(Car row);
List<Car> selectByExample(CarExample example);
Car selectByPrimaryKey(Long id);
int updateByExampleSelective(@Param("row") Car row, @Param("example") CarExample example);
int updateByExample(@Param("row") Car row, @Param("example") CarExample example);
int updateByPrimaryKeySelective(Car row);
int updateByPrimaryKey(Car row);
}
测试增强版
其中的CarExample 是用来封装查询条件的
@Test
public void testSelect() {
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
//1.查一个
Car car = mapper.selectByPrimaryKey(119l);
System.out.println(car);
//2.查询所有:没有条件
List<Car> cars = mapper.selectByExample(null);
cars.forEach(System.out::println);
//3.按照条件查询
// 通过CarExample对象封装条件
CarExample carExample = new CarExample();
//创建查询条件
//select * from t_car where brand like 'BYDQ' and guide_price > 20.0;
carExample.createCriteria().andBrandLike("BYDQ")
.andGuidePriceGreaterThan(BigDecimal.valueOf(20.0));
//添加or
//前后添加括号
carExample.or().andCarTypeEqualTo("电车");
//执行查询
List<Car> carsByExample = mapper.selectByExample(carExample);
carsByExample.forEach(System.out::println);
sqlSession.close();
}
这种查询风格是 QBC Query By Criteria,比较面向对象
2023-06-23T13:05:16.446005Z 141 Query
select
id, car_num, brand, guide_price, produce_time, car_type
from
t_car
WHERE
( brand like 'BYDQ' and guide_price > 20.0 )
or
( car_type = '电车' )
Mybatis分页插件
分页查询的原理:
- 页码:pageNum
- 每页显示记录条数:pageSize
这两个数据会从前端传递给服务器,告知服务器具体要返回的是哪几条记录
uri?pageNum=1&pageSize=10
- MySQL limit分页
limit beginIndex,pageSize
select * from t_car limit 0,3; # 展示1 2 3 条记录
select * from t_car limit 2; #展示 1 2 也就是起始下标默认从0开始
假设每页显示10条记录:
#第1页:
limit 0,3; #(0,1,2)
#第2页:
limit 3,3; #(3,4,5)
#第3页:
limit 6,3; #(6,7,8)
#第n页:
limit (pageNum - 1) * pageSize , pageSize;
List<Car> selectByPage(@Param("beginIndex") int beginIndex,
@Param("pageSize") int pageSize);
<select id="selectByPage" resultType="Car">
select * from t_car limit #{beginIndex},#{pageSize}
</select>
@Test
public void selectByPageTest() {
int pageNum = 2;
int pageSize = 3;
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByPage((pageNum - 1) * pageSize, pageSize);
cars.forEach(System.out::println);
}
获取记录不难,获取分页相关的数据比较难,可以借助mybatis的PageHelper插件
分页相关的数据:总记录条数多少、是否有上/下页?、分页导航要显示多少个
pageHelper
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.1</version>
</dependency>
在mybatis-config中typeAliases标签下添加:
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
List<Car> selectAll();
<select id="selectAll" resultType="Car">
select * from t_car <!--使用PageHelper不能加 ; -->
</select>
@Test
public void selectAllTest() {
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
//在select语句之前开启分页功能
int pageNum = 2;
int pageSize = 3;
PageHelper.startPage(pageNum,pageSize);
List<Car> cars = mapper.selectAll();
cars.forEach(System.out::println);
}
获取分页相关的数据,在查询后创建PageInfo对象:
@Test
public void selectAllTest() {
SqlSession sqlSession = SQLSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
//在select语句之前开启分页功能
int pageNum = 2;
int pageSize = 3;
PageHelper.startPage(pageNum,pageSize);
List<Car> cars = mapper.selectAll();
//获取分页信息对象
int pageNavigationNum = 10;
PageInfo<Car> carPageInfo = new PageInfo<>(cars, pageNavigationNum);
System.out.println(carPageInfo);
}
获取分页相关的信息:
PageInfo{
pageNum=2, pageSize=3, size=3, startRow=4, endRow=6, total=111, pages=37,
list = Page{count=true, pageNum=2, pageSize=3, startRow=3, endRow=6, total=111, pages=37, reasonable=false, pageSizeZero=false}
[
Car{id = 10, carNum = 1003, brand = AD, guidePrice = 30.00, produceTime = 2000-10-11, carType = 燃油车}, Car{id = 11, carNum = 1003, brand = AD, guidePrice = 30.00, produceTime = 2000-10-11, carType = 燃油车}, Car{id = 12, carNum = 1003, brand = AD, guidePrice = 30.00, produceTime = 2000-10-11, carType = 燃油车}
],
prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true, navigatePages=10, navigateFirstPage=1, navigateLastPage=10,
navigatepageNums=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
Mybatis注解式开发
mybatis中也提供了注解式开发方式,采用注解可以减少Sql映射文件的配置。
当然,使用注解式开发的话,sql语句是写在java程序中的,这种方式也会给sql语句的维护带来成本。
官方是这么说的:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
使用注解编写复杂的SQL是这样的:
原则:简单sql可以注解。复杂sql使用xml。
注意:对于同一条SQL,不能同时以注解和XML方式配置,会报错。
public interface CarMapper {
@Insert("insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})")
int insert(Car car);
@Delete("delete from t_car where id = #{id}")
int deleteById(Long id);
@Update("update t_car set car_num = #{carNum},brand = #{brand} where id = #{id}")
int update(Car car);
@Select("select * from t_car where id = #{id}")
List<Car> selectById(Long id);
}
mybatis-config中配置了驼峰命名,Select注解就可以正确调用set方法,给pojo类属性赋值
如果没有配置驼峰命名,可以使用Results注解
@Select("select * from t_car where id = #{id}")
@Results({
@Result(property = "carNum",column = "car_num"),
@Result(property = "guidePrice",column = "guide_price"),
@Result(property = "carType",column = "car_type"),
})
List<Car> selectById(Long id);
标签:String,car,brand,sqlSession,Mybatis,id,select
From: https://www.cnblogs.com/euneirophran/p/18073915