1. ORM思想
ORM(对象关系映射)
Object:JVM虚拟机中创建的对象
Relational:关于Object对象和数据
Mapping(映射):映射
一个简单的实例
在Java
虚拟机中创建的类
class User{
private Integer id;
private String name;
private Integer age;
}
在数据库中的表结构
通过这种映射关系,可以将Java对象转化为数据库表中的一条记录
因为Mybatis
是一个半自动的ORM,这是因为Mybatis
中的SQL语句是需要程序员自己编写的。
2. 开发入门
- 在Mybatis中有一个很重要的对象,这个对象是
SqlSessionFactory
SqlSessionFactory
需要通过XML
文件进行读取
编写Mybatis核心配置文件,注意这个文件不是必须叫做mybatis-config.xml
,可以用其他的名字。
这个文件存放的位置也不是固定的,可以随意,但是在一般情况下,会放到类的根路径下。
<?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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
关于Mybatis的配置文件
Mybatis-config.xml:核心配置文件,用来配置连接数据库、事务管理器等的信息(一个)
xxxMapper.xml:专门用来编写SQL语句的配置文件(根据业务需求有0..n个)一张来说,一张表对应一个Mapper
修改以上基础配置信息即可,要注意的是resouces
属性会自动从类的根路径下开始查找
接着使用Mybatis的类库编写mybatis程序,连接数据库,做CRUD
在Mybatis中负责执行SQL语句的对象叫做
SqlSession
,它是专门用来执行SQL的语句,是一个Java和数据库之间的一次会话,而如果想要获取SqlSession
对象,需要先获取SqlSessionFactorty
对象,通过SqlSessionFactory
工厂来生产SqlSession
对象,而如果想要获取SqlSessionFactory
对象,则还要获取SqlSessionFactoryBuilder
对象,然后调用build()
方法即可
核心对象:
- SqlSessionFactoryBuilder
- SqlSessionFactory
- SqlSession
编写Mapper文件
<?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="cn.edu.scau.cmi.zhangtingjie.orm.mybatis.StudentDAO">
<insert id="addStudent">
insert into student(id,name,tutor) values(?,?,?)
</insert>
</mapper>
public static void testMybatis(){
SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
try {
// 根据config.xml中的信息,解析得到对应的会话工厂
// 所谓的会话工厂:就是基于xml信息,通过这些xml信息来给SqlSessionFactory中的一些信息赋值
SqlSessionFactory sqlSessionFactory = factoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
//输入流,指向mybatis的核心配置文件
SqlSession sqlSession = sqlSessionFactory.openSession();//获取会话对象
int count = sqlSession.insert("addStudent");//执行对应的sql语句,不支持自动提交
System.out.println("插入了"+count+"条语句");
//需要在这里手动提交
sqlSession.commit();
} catch (IOException e) {
e.printStackTrace();
}
}
一般情况下都是一个数据库对应一个sqlSession
对象
Mybatis入门程序的细节问题
resouces
:从类的根路径下开始加载加载
mybatis-config.xml
的流需要手动关闭吗?public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { SqlSessionFactory var5; try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); var5 = this.build(parser.parse()); } try { if (inputStream != null) { inputStream.close();//可以看到这个方法帮我们自动关闭了 } } }
因此,由于这个方法会帮我们关闭流,因此流就不需要我们手动关闭了,因此,你也可以认为这个流是一次性的,运行完了配置之后就不会再使用的了。
ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml"); //用系统类加载器,从类路径中加载资源
通过源码分析可以发现其实
Resources.getResourceAsStream
是调用了ClassLoader
的getResouces()
方法,只不过做了一层封装。<mapper url="file://D:/student.xml"/>
这个属性标注上了可以从绝对路径中加载资源
3. 事务管理机制剖析
<transactionManager type="JDBC"/>
可以在核心配置文件中为JDBC配置了事务管理器,而type可以写JDBC
和MANAGED
其中的一个,而JDBC底层的管理机制还是借助于JDBC
的事务处理机制,比如
connection.setAutoCommit(false);//管理事务
....业务处理....
coneection.commit();
MANAGED
事务管理器:Mybatis
不再负责事务的管理了。事务管理交给其他容器来负责,例如Spring
,对于我们当前的单纯只有Mybatis
的情况下,如果配置为MANAGED
那么事务这块就没有开启了。
使用JDBC
事务管理器的话,底层创建的事务管理器对象:JdbcTrasaction
对象,如果编写的代码是
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//表示没有开启事务,因为这种方式不会执行conn.setAutoCommit(false);
//那么默认的是setAutoCommit(true),表示自动提交事务,这种情况就叫做没有事务管理
为了帮助记忆,我们可以在这里的标志符,false表示关闭自动提交,true表示开启自动提交
这主要是因为connection.getAutoCommit()
原来默认就是true
的,而你如果传进来一个true
的话,那么
if(true != true){
//这个条件一定不成立,那么就一定不会执行里面的代码,如果你传进来一个false,就会传进来这个代码
connection.setAutoCommit(false);//对于jdbc而言,就是关闭提交了
}
4. Junit与logback的使用
一般来说基本的测试框架如下:
@Test
public void testAddStudent(){
//单元测试中有两个重要的概念
//一个是实际值
//一个是期望值
int actual = 2;
int expected = 3;
//加断言进行测试
Assert.assertEquals(expected,actual);
}
- 在Mybatis中引用Junit
只需要直接导入Junit
即可:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
</dependencies>
- 在Mybatis中集成loback
首先了解一下常见的集成日志组件
- SLF4J:是一个日志标准,其中有一个框架叫做logback,实现了相关的规范
- LOG4J
- LOG4J2
- STDOUT_LOGGING标准日志输出,由Mybatis实现
配置实例
<settings>
<!--当没有指定的时候将会自动查找,将会按照你集成的环境进行查找-->
<setting name="logImpl" value="SLF4J"/>
<setting name="logImpl" value="LOG4J"/>
<setting name="logImpl" value="LOG4J2"/>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
这个标签应该要出现在environments
这些规定是在dtd
文件中规定的。
集成logback日志框架:
- 导入logback的依赖
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
- 配置logback的配置文件,名字是
logback.xml或者logback-test.xml
,不能是其他的名字,这个配置文件必须放到类的根路径下,不能是其他位置,它的优先级是先找logback-test.xml
然后再找logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 配置文件修改时重新加载,默认true -->
<configuration debug="false">
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder" charset="UTF-8">
<!-- 输出日志记录格式 -->
<pattern>[%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>
5. 封装Mybatis工具类
public class SqlSessionUtil {
private static SqlSessionFactory sqlSessionFactory;
static {
//类加载时执行
//类加载时解析mybatis-config.xml文件
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
//为了防止new对象
private SqlSessionUtil(){}
/**
* 获取sqlSession对象
* @return
*/
public static SqlSession openSession(){
return sqlSessionFactory.openSession(false);
}
}
6. 使用Java对象对insert操作进行传参
6.1 使用Map集合
在JDBC中占位符采用的是?而在mybatis的则是?
等效的写法是#{}
,必须使用#{}
来代替?
public static void testMybatis(){
SqlSession sqlSession = SqlSessionUtil.openSession();
try{
//需要一个对象进行数据的封装
Map<String,Object> map = new HashMap<>();
map.put("id","6");
map.put("name","周八");
map.put("tutor","周八师");
sqlSession.insert("addStudent",map);
}finally {
sqlSession.close();
}
}
<insert id = "addStudent">
insert into student(id,name,tutor) values(#{id},#{name},#{tutor});
</insert>
Java程序中可以使用Map
对SQL
语句的占位符传值,如果它找不到这个key,那么它就会取到一个null
其实这也印证了一点,它底层实际上是通过包装Map
集合来对数据进行存储使用的。
6.2 使用pojo对象
//如果使用的是domain的对象
Student student = new Student("6","name","tutor");
sqlSession.insert("addStudent",student);
如果通过domain
对象进行取值绑定数据的情况下,其底层是调用其getXXX()
方法来获取相关的数据的。因此为了做到通过pojo
绑定数据,因此必须为实体类提供getXXX()
方法。而在#{}
里面提供的属性名是xXX
(注意大小写)
也就是说mybatis
在底层给?
传值的时候,先要获取值,调用了pojo
的get
方法 。
6.3 CURD汇总
<?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="cn.edu.scau.cmi.zhangtingjie.orm.mybatis.StudentDAO">
<insert id = "addStudent">
insert into student(id,name,tutor) values(#{id},#{name},#{tutor});
</insert>
<update id="updateStudent">
update student
set name = #{name},tutor = #{tutor}
where id = #{id};
</update>
<delete id="deleteStudent">
delete from student where id = #{id};
</delete>
<select id="findStudent" resultType="cn.edu.scau.cmi.zhangtingjie.orm.domain.Student">
select * from student where id = #{id};
</select>
</mapper>
public interface StudentDAO {
/**
* 增加学生
* @param student
* @return
*/
boolean addStudent(Student student);
/**
* 删除学生
* @param id
* @return
*/
boolean deleteStudent(String id);
/**
* 查询学生
* @param id
* @return
*/
Student findStudent(String id);
/**
* 更新学生信息
* @param student
* @return
*/
boolean updateStudent(Student student);
}
6.4 查询一个
//查询一个
try (SqlSession sqlSession = SqlSessionUtil.openSession()) {
sqlSession.selectOne("selectOne", 1);
}
执行DQL语句进行查询,根据ID进行查询,返回结果一定会是一条,Mybatis
底层执行了select
语句之后,一定会返回一个结果集对象ResultSet
,它在JDBC
中是ResultSet
,接下来就是Mybatis
应该从ResultSet
中取出数据,封装为java对象 。
因此对于查询语句,一般来说需要封装对象类型,对于查询回来的字段,利用orm
原理,将字段映射回java
对象。
<select id="selectOne" resultType="domain.Student">
select * from student where id = #{id};
</select>
因此在这里需要给mybatis透露一个信息,关于返回数据的类型,它会自动封装相关的数据到对象里面,然后使用。
一定要注意,当你的数据库中存在有下划线的时候,这时候会出现赋值失败的原因,可以分析一下
select * from t_car where id = 1;
当执行完毕之后,返回的结果集是
id car_num brand guide_price produce_time car_type
Car
类的属性名
carNum,guidePrice,produceTime,carType
可以看到在这种情况下,这些列名和Car
类中的属性名对不上,因此在这种方法下赋值不上去。
注意:在学完了boot回来之后,发现之前是没有这个问题的,这是因为boot中封装了相关的驼峰命名和下划线命名的映射相关API,因此自动完成了映射,不需要我们完成这些处理
那么在这里的话,为了让我们的结果集对上,这样的话就需要使用as
关键字,利用as
关键字重命名,让对象属性和数据库字段完全对上。
select id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
6.5 查询所有
<select id="selectAll" resultType="domain.Student">
select * from student;
</select>
<!--注意:如果有下划线的话一定还是要取别名的,而我这个表是没有下划线命名的,因此没有做-->
public void testSelectAll(){
SqlSession sqlSession = SqlSessionUtil.openSession();
List<Student> selectAll = sqlSession.selectList("selectAll");
for (Student student : selectAll) {
System.out.println(student);
}
}
注意resultType
要指定元素的类型,这是因为selectList()
,你需要一个列表,但是它无法确定你列表内部所需要的元素,所以的话你要给它提供这个信息。
7. namespace的作用
在一种情况下,在两个mapper.xml文件中存在两个完全相同的sqlId
,这时候Mybatis
就会蒙圈,不知道你指的这个SqlID
到底具体是哪个SQL
,这时候就会抛出无效映射的异常。
namespace的作用主要是用来防止SQL
的ID冲突。
因此实际上在Mybatis中执行的SQL的时候,它有两个步骤,首先它要查找到这个sqlId,如果你的SqlId
在全局是唯一的,那么它就能直接执行。如果你的SqlId
不是全局唯一的,而是存在于多个命名空间之中,这时候就需要先通过命名空间找到这个SqlId
,然后再通过SqlId
找到对应的SQL
语句进行执行。
8. Mybatis核心配置文件配置多环境
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--根标记是configuration,标志这个文件是配置文件,还有的有mapper,mapper文件则是mapper标记-->
<!--同时需要在xml文件提供它的dtd文件,它用来检查xml文件的合法性-->
<environments default="development">
<!--默认环境:可以用来切换SqlSeesionFactory,如果你没有指定环境,那么就使用你default指定的值-->
<!--配置多个环境,可以存在有多个environment,切换environment只需要修改选择的值-->
<!--在mybatis中,一般一个环境对应一个数据库-->
<!--一般来说,一个数据库会对应一个SqlSessionFactory对象-->
<!--一个环境environment会对应一个SqlSessionFactory对象-->
<!--这是因为一个SqlSessionFactory是由builder来的,而builder是通过xml文件读出来的-->
</environments>
public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
同时也可以通过这种形式进行环境的指定,对environment
进行指定,但是这种就相当于在代码中写了配置信息,这是不符合开闭原则的。
<transactionManager type="JDBC"/>
作用:配置事务管理器,指定mybatis具体使用什么方式去管理事务
public interface Transaction {
Connection getConnection() throws SQLException;
void commit() throws SQLException;
void rollback() throws SQLException;
void close() throws SQLException;
Integer getTimeout() throws SQLException;
}
JDBC事务管理器底层源代码
public class JdbcTransaction implements Transaction {
private static final Log log = LogFactory.getLog(JdbcTransaction.class);
protected Connection connection;
protected DataSource dataSource;
protected TransactionIsolationLevel level;
protected boolean autoCommit;
protected boolean skipSetAutoCommitOnClose;
public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
this(ds, desiredLevel, desiredAutoCommit, false);
}
public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit, boolean skipSetAutoCommitOnClose) {
dataSource = ds;
level = desiredLevel;
autoCommit = desiredAutoCommit;
this.skipSetAutoCommitOnClose = skipSetAutoCommitOnClose;
}
public JdbcTransaction(Connection connection) {
this.connection = connection;
}
@Override
public Connection getConnection() throws SQLException {
if (connection == null) {
openConnection();
}
return connection;
}
@Override
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + connection + "]");
}
connection.commit();
}
}
@Override
public void rollback() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Rolling back JDBC Connection [" + connection + "]");
}
connection.rollback();
}
}
@Override
public void close() throws SQLException {
if (connection != null) {
resetAutoCommit();
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + connection + "]");
}
connection.close();
}
}
protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
try {
if (connection.getAutoCommit() != desiredAutoCommit) {
if (log.isDebugEnabled()) {
log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");
}
connection.setAutoCommit(desiredAutoCommit);
}
} catch (SQLException e) {
// Only a very poorly implemented driver would fail here,
// and there's not much we can do about that.
throw new TransactionException("Error configuring AutoCommit. "
+ "Your driver may not support getAutoCommit() or setAutoCommit(). "
+ "Requested setting: " + desiredAutoCommit + ". Cause: " + e, e);
}
}
protected void resetAutoCommit() {
try {
if (!skipSetAutoCommitOnClose && !connection.getAutoCommit()) {
// MyBatis does not call commit/rollback on a connection if just selects were performed.
// Some databases start transactions with select statements
// and they mandate a commit/rollback before closing the connection.
// A workaround is setting the autocommit to true before closing the connection.
// Sybase throws an exception here.
if (log.isDebugEnabled()) {
log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]");
}
connection.setAutoCommit(true);
}
} catch (SQLException e) {
if (log.isDebugEnabled()) {
log.debug("Error resetting autocommit to true "
+ "before closing the connection. Cause: " + e);
}
}
}
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
connection = dataSource.getConnection();
if (level != null) {
connection.setTransactionIsolation(level.getLevel());
}
setDesiredAutoCommit(autoCommit);
}
@Override
public Integer getTimeout() throws SQLException {
return null;
}
}
public class ManagedTransaction implements Transaction {
private static final Log log = LogFactory.getLog(ManagedTransaction.class);
private DataSource dataSource;
private TransactionIsolationLevel level;
private Connection connection;
private final boolean closeConnection;
public ManagedTransaction(Connection connection, boolean closeConnection) {
this.connection = connection;
this.closeConnection = closeConnection;
}
public ManagedTransaction(DataSource ds, TransactionIsolationLevel level, boolean closeConnection) {
this.dataSource = ds;
this.level = level;
this.closeConnection = closeConnection;
}
@Override
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
@Override
public void commit() throws SQLException {
// Does nothing
}
@Override
public void rollback() throws SQLException {
// Does nothing
}
@Override
public void close() throws SQLException {
if (this.closeConnection && this.connection != null) {
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + this.connection + "]");
}
this.connection.close();
}
}
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
this.connection = this.dataSource.getConnection();
if (this.level != null) {
this.connection.setTransactionIsolation(this.level.getLevel());
}
}
@Override
public Integer getTimeout() throws SQLException {
return null;
}
}
可以看到其底层是通过接口实现不同子类来实现的。
数据源配置:
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/orm?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
数据源的作用是为程序提供conncetion对象,但凡是给程序提供连接对象的都是数据源,其实是一套规范
public interface DataSource extends CommonDataSource, Wrapper {
Connection getConnection() throws SQLException;
Connection getConnection(String username, String password)
throws SQLException;
}
如果我们自己编写了实现子类,就可以实现一个数据源组件。数据库连接池就是提供连接对象的,所以数据库连接池就是一个数据源。因此,只要实现了这一套规范,我们就认为它是一个数据源组件
常见的数据源组件(数据库连接池):如Durid、c3p0、dbcp
<dataSource type="POOLED">
<!--这里指明了数据库连接池的组件-->
- POOLED:Mybatis自己实现的连接池
- UNPOOLED:不使用数据库连接池技术,每次都创建新的连接池对象
- JNDI:集成第三方连接池
大部分的JNDI都实现了JNDI规范,Java命名目录接口,JavaNamingAndDirectoryInterface
为什么要实现这个规范呢,这是为了便于各种第三方框架进行集成,所谓命名目录接口规范,这是为了各种框架在集成的时候,能够有一套统一的标准对这些组件进行路径寻址,以Tomcat集成Mybatis为例,Tomcat中实现了这个规范,那么Tomcat就能够在集成了C3P0、Durid的情况下,能够为Mybatis提供路径寻址,从而能够使得不同的组件集合起来工作,这就是规范的作用。
POOLED
和UNPOOLED
的区别主要是在底层进行
JDBC
处理的不同对于
POOLED
:它在使用完一个connection
的时候,会有一个return
操作,也就是说建立好了之后把连接对象返回到池中,方便下次调用,同时连接池也类似于线程池,它也可以控制连接的数量,如果超过了这个数量,就会导致线程阻塞等待获取连接。如果等待超时,就会把现有的连接对象拿回到连接池中,给别人用。对于
UNPOOLED
:每次的JDBC操作都新建一个conncetion
- 具体参数的配置
<!--设置好参数可以让连接池发挥更好的效果-->
<!--简单来说就是进行调参-->
<!--连接池当中最多的正在使用的连接对象的数量上限:最多有多少个连接在活动状态,默认是10-->
<property name="poolMaximumActiveConnections" value="10"/>
<!--在强制返回之前,池中连接过期的时间,也就是当一个线程等待了这个时间之后,会强制剥夺一个连接到连接池中-->
<property name="poolMaximumCheckoutTime" value="20000"/>
<!--如果获取连接花费了相当长的时间,连接池会打印日志状态并且重新尝试获取一个日志-->
<property name="poolTimeToWait" value="20000"/>
<!--配置最多的空闲数量,当多于这个数量的时候,就关闭一个-->
<property name="poolMaximumIdleConnections" value="5"/>
<!--使得mybatis的配置更加灵活。-->
<properties>
<property name="key" value="value"/>
</properties>