- 1、MyBatis是什么?
- 2、JDBC编程有哪些缺陷?MyBatis又是如何改进的?
- 3、MyBatis与Hibernate的区别在哪?
- 4、MyBatis的优缺点
- 5、MyBatis的执行流程?
- 6、#{}和${}的区别
- 7、如何获取生成的主键?
- 8、当实体类中的属性名和表中的字段名不一样怎么办?
- 9、什么是MyBatis的接口绑定?有哪些实现方式?
- 10、Mybatis接口方法参数绑定有哪几种方式?
- 11、使用MyBatis的mapper接口调用时有哪些要求?
- 12、Mybatis中的mapper接口都没有实现类,是如何执行里面的方法的?
- 13、MyBatis动态sql是做什么的?都有哪些动态sql?简述一下动态sql的执行原理?
- 14、MyBatis的一级、二级缓存是什么?
MyBatis是一款优秀的持久层框架、一个半ORM(对象关系映射)框架。它支持定制化SQL、存储过程、高级映射、缓存机制,同时MyBatis几乎避免了JDBC代码中手动设置参数以及获取结果集等繁杂操作的过程。
2、JDBC编程有哪些缺陷?MyBatis又是如何改进的?-
JDBC,Dbutils,JdbcTemplate(spring提供),执行SQL语句的步骤:
-
JDBC编程中频繁创建、释放数据库连接对象,容易造成系统资源浪费,影响系统性能。可以使用连接池解决这个问题。
Mybatis:在mybatis-config.xml中配置数据库连接池,使用连接池管理数据库连接。
-
JDBC编程中 sql 语句写在代码中造成代码不易维护,实际应用场景中sql的变化可能较大,sql变动需要改变java代码。
Mybatis:将SQL语句配置在XXXXmapper.xml映射文件中,与java代码分离。
-
JDBC编程中向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
Mybatis:Mybatis中自动将java对象映射至sql语句。
-
JDBC编程中对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
Mybatis:Mybatis中自动将sql执行结果映射至java对象。
Hibernate:是一个全自动全映射的ORM(Object Relation Mapping)框架,旨在消除SQL,执行如下图:
-
不方便定制化SQL:如果我们想定制SQL,还需要学习Hibernate提供的HQL。
-
全映射不太好:大量字段的POJO进行部分映射比较困难,导致数据库性能下降。
举个例子,假如一张员工表有15个字段,我们只需要查其中的两个字段,如果使用Hibernate将会把所有字段都查询并返回(全映射),如果想定制化,也需要学习HQL。
-
内部自动生成的SQL:不易做特殊优化。
Mybatis:是一个半自动的轻量级ORM(Object Relation Mapping)框架,执行如下图:
- 手动编写SQL:方便做SQL语句定制化。
优点
-
基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除SQL与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
-
与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接。
-
很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
-
提供映射标签,支持对象与数据库的字段映射;提供对象关系映射标签,支持对象关系组件维护。
-
能够与Spring很好的集成。
缺点
-
SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
-
SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
-
1、读取主配置文件:mybatis-config.xml
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); -
2、通过主配置文件读取到映射配置文件:xxxmapper.xml
-
3、使用会话工厂的创建器创建会话工厂:SqlSessionFactoryBuilder—>SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
-
4、通过会话工厂创建会话对象:SqlSessionFactory—> SqlSession
SqlSession session = sqlSessionFactory.openSession();
-
5、通过会话对象的getMapper()方法获取mapper接口的代理对象:
UserMapper userMapper = session.getMapper(UserMapper.class);
-
6、通过代理对象执行mapper中的方法:
User user=userMapper.selectById(1);
-
7、对返回结果进行封装
先看一下Mybatis的执行流程:
-
#{}是占位符,预编译处理,可以防止SQL注入;
-
${}是拼接符,字符串替换,没有预编译处理,不能防止SQL注入。
-
Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
-
Mybatis在处理${}时,是原值传入,就是把${}替换成变量的值,相当于JDBC中的Statement编译。
-
#{} 的变量替换是在DBMS 中,变量替换后,#{} 对应的变量自动加上单引号;
-
${} 的变量替换是在 DBMS 外,变量替换后,${} 对应的变量不会加上单引号;
注意:在Mybatis框架下,不能使用预编译的地方是不可以使用#{}传递参数的,只能使用${}传递参数,举例如下:
//查询指定年份的工资表
select * from ${year}_salary where id=#{}
- 在获取year参数的时候,是不可以使用#{}的,因为不支持预编译。
- 在获取id参数的时候,是可以使用#{}的,因为支持预编译。
这个分两种情况:
- 第一种:支持自增主键的数据库,例如:MySQL
- 第二种:不支持自增主键的数据库,例如:Oracle
第一种:MySQL
-
useGeneratedKeys:使用自增主键获取主键值策略(默认为false)
-
keyProperty:指定主键对应的属性
insert into tb_user(name,age,sex) values(#{name},#{age},#{sex})
第二种:Oracle
-
Oracle不支持自增,Oracle使用序列来模拟自增,每次插入的数据的主键是从序列中拿到的值
-
使用select * from tb_user_sequences;来查询对应表中的所有序列。
select USER_SEQ.nextval from dual insert into tb_user(id,name,age,sex) values(#{id},#{name},#{age},#{sex})
方式一:通过在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
方式二:通过映射配置文件中
方式三:在mybatis全局配置文件中开启驼峰命名规则。
-
注意:这种方式只能解决数据库字段命名中间使用下划线,对应实体类属性是使用驼峰命名,才可以:
数据库字段为:last_name 实体类的字段为:lastName
接口绑定:我们定义接口,然后把接口里面的方法和SQL语句绑定,我们调用接口方法的时候,最终会执行绑定的SQL语句。
接口绑定有两种实现方式:注解绑定,xml绑定。
-
通过注解绑定:就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来实现接口绑定,全局配置文件中使用class属性指定mapper,这种绑定方式不太好,因为如果要改SQL语句的时候需要改源码。
public interface UserMapper { @Select("select * from tb_user") public User selectById(int id); } -
通过在xml绑定:指定xml映射文件里面的namespace必须为接口的全限定类名,同时接口的方法名和SQL语句的id一一对应,全局配置文件中使用resource属性指定映射配置文件。
接口方法有单一参数:
-
如果接口方法是单一参数,xml文件中直接使用#{}获取即可,如下:
public interface UserMapper{ public User selectById(Integer id); }
接口方法有多个参数:如果是接口方法是多个参数,会被封装为map
-
方式一:直接使用位置参数获取,位置1是param1,位置2是param2,以此类推:
public interface UserMapper{ public User selectByIdAndName(Integer id,String name); } -
方式二:使用@Param注解来指定封装时使用的key,然后使用#{key}就可以取出map中的值,如下:
public interface UserMapper{ public User selectByIdAndName(@Param("id")Integer id,@Param("name")String name); } -
方式三:可以直接传递一个对象,如果参数刚好可以使用这个对象属性来封装
public interface UserMapper{ public User selectByIdAndName(User user); } -
方式四:可以传递一个map,使用这个map来封装要传递的属性。
public interface UserMapper{ public User selectByIdAndName(Mapmap); } //测试类 map=new HashMap<>(); map.put("id",1); map.put("name","aismall")
1、映射配置文件中的namespace的值应该是对应mapper接口的全限定类名。
.......
2、Mapper接口中的方法名和映射配置文件中定义的sql语句id一一对应。
3、Mapper接口中方法的输入参数类型和映射配置文件中定义的每个sql语句的parameterType的类型相同。
4、Mapper接口方法的输出参数类型和映射配置文件中定义的每个sql语句的resultType的类型相同。
-
注意下面三点:
-
1、如果返回的类型是一个集合的话,resultType的类型应该是集合中包含类的类型,例如下面的情形,resultType="com.aismall.pojo.User"
public List
selectByName(String name); -
2、 如果返回值的类型是一个map的话(返回一行数据),resultType的类型就直接指定为map,例如下面的情形,resultType="map"
public Map
selectById(int id); -
3、 如果返回值的类型是一个map的话(返回多行数据),resultType的类型应该map中用来封装数据类的类型,但是,还应该在mapper中的方法上指定,谁做为key存在,例如下面的情形,resultType="com.aismall.pojo.User"
@MapKey("id")// 使用id作为key,也可以使用别的属性最为key public MapselectByName(String name);
Mybatis中是接口+xml的方式,这个接口我们并没有实现它,可是我们却可以直接调接口的方法操作数据库,其中的原理到底是为什么呢?
- 先说答案:Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象MapperProxy,代理对象MapperProxy会拦截接口方法调用,转而执行方法对应的sql语句,然后将sql执行结果返回。
具体分析:
先看一段代码:
public void test() throws IOException{
InputStream inputStream = Resources.getResourceAsSteream("mybatis-config.xml");
//读取mybatis配置文件创建SqlssionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取对应的mapper
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//执行查询语句并返回结果
User user = mapper.selectById(1);
}
我们重点关注:build()方法,openSession()方法,getMapper方法
- SqlSessionFactory类中的 build()方法:
//debug进入这个方法,根据传入的参数为inputStream会选择这个重载的build方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
//声明一个SqlSessionFactory对象
SqlSessionFactory var5;
try {
//创建一个XMLConfigBuilder对象 创建一个解析XML文件的对象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 注意:XMLConfigBuilder类中的parse()方法会返回一个Configuration对象
// 根据传入的值为一个Configuration对象,执行下面的的build重载方法
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
//由于DefaultSqlSessionFactory继承了SqlSessionFactory,所以用父类来接收子类的对象
return var5;
}
// 可以接收Configuration对象的重载方法
public SqlSessionFactory build(Configuration config) {
//根据配置返回一个持有Configuration对象的DefaultSqlSessionFactory对象
return new DefaultSqlSessionFactory(config);
}
- DefaultSqlSessionFactory类中的openSession()方法
// 根据传入的参数为空,会选择这个重载的openSession方法
public SqlSession openSession() {
// 调用这个openSessionFromDataSource方法
return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
// 注意最后会返回一个DefaultSqlSession对象
DefaultSqlSession var8;
try {
// 获取环境配置
Environment environment = this.configuration.getEnvironment();
// 根据环境获取一个创建事务的工厂类
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
// 获取事务:里面设置了事务的隔离级别,是否自动提交
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 获得一个执行器
Executor executor = this.configuration.newExecutor(tx, execType);
// 将前面设置好的各种配置和事务一起封装到一个DefaultSqlSession对象里面并返回
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
- DefaultSqlSession类中的getMapper方法
// 返回值类型是形参类型:Mapper类的代理类类型 publicT getMapper(Class type) { // 调用Configuration类中的getMapper方法 return this.configuration.getMapper(type, this); }
- Configuration类中的getMapper方法
// 返回值类型是形参类型:Mapper类的代理类类型 publicT getMapper(Class type, SqlSession sqlSession) { //调用MapperRegistry类中的getMapper方法 return this.mapperRegistry.getMapper(type, sqlSession); }
- MapperRegistry类中的getMapper方法
// 返回值类型是形参类型:Mapper类的代理类类型 publicT getMapper(Class type, SqlSession sqlSession) { // 获取一个代理工厂类,根据传入类的类型(Mapper接口类型的) MapperProxyFactory mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type); //为空则进入 if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } else { try { //调用mapperProxyFactory.newInstance方法,返回一个MapperProxy代理类对象 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception var5) { throw new BindingException("Error getting mapper instance. Cause: " + var5, var5); } } }
执行流程图如下:
-
Mybatis动态sql可以让我们在xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。
-
Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。
-
其执行原理为,使用OGNL(对象导航图语言Object Graph Navigation Language)从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。
-
一级缓存:会话级别的缓存,同一个SqlSession会话共享。采用 PerpetualCache,HashMap 存储,MyBatis默认打开一级缓存,其存储作用域为当前sqlSession会话对象,当 sqlSession flush 或 close 之后,该 sqlSession 中的所有 Cache 就将清空。可以调用 clearCache();,手动清理缓存。
-
二级缓存:namespace界别的缓存,同一个namespace中的所有方法共享。默认也是采用 PerpetualCache,HashMap 存储,二级缓存的存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache,默认不打开二级缓存,开启二级缓存之后(在mybatis主配置文件中添加
;其次在对应的mapper映射文件中添加 ),对应的实体类需要实现Serializable序列化接口(可用来保存对象的状态)。 -
对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)进行了C/U/D 操作后,默认该作用域下所有缓存将被清理掉。
-
缓存的查询顺序为:二级——>一级——>数据库



