简介
Mybatis,原名iBatis,是Apache开源项目,10年迁移到google code改名Mybatis,13年迁移到github。是dao层的框架,用于数据库访问的各操作。
JDBC的缺点
需要创建很多对象、注册驱动、关闭资源这些重复性的代码,费事;
sql语句与业务逻辑代码混在一起
quick start(查询)
先在数据库建张表,不需要多复杂,重在体会过程。还用之前的老例子
引入必要依赖:
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
</dependencies>
在entity(或domain、pojo)包下创建实体类,其属性对应表中字段名,只不过数据库是“_”连接多个单词,属性名是驼峰命名法。并生成对应属性的get、set方法。
然后创建dao类,并在同一个包下创建与该类同名的xml映射(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="ind.deng.mybatis.dao.BookDao">
<select id="selectBookById" resultType="ind.deng.mybatis.entity.Book">
select * from book where id = #{bookId}
</select>
</mapper>
前面四行是固定语法,每次创建mapper文件直接copy即可。
其中的"http://mybatis.org/dtd/mybatis-3-mapper.dtd"是约束文件,限定当前文件内可以使用的标签和属性及其前后顺序
mapper跟标签namespace属性必须有,不能为空,其值为对应dao接口的全限定名(约定)
mapper标签里可以有,,,标签
id值为dao中的方法名(约定),resultType值为对应实体类的全限定名,查询操作才有resultType
注意,sql语句末尾没有分号。#{bookId}是占位符,接收传递过来的参数
注:#{}占位符底层使用的是preparedStatement,效率高且防止sql注入,也能根据参数类型而变化,即string类型也不用自己加单引号
还有另一种KaTeX parse error: Expected 'EOF', got '#' at position 10: {}占位符,功能和#̲{}一致,但底层使用state…{}中的内容和sql语句拼接,类似c语言中的宏定义。
效率低且有注入风险。此外,${}就算只有一个普通参数也得用@Param
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
mapper文件在dao包里(在resources下就没这问题)。要想让编译器找到,需要在pom文件加上以上几行代码(和dependencies平级)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="iquygned787923"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="ind/deng/mybatis/dao/BookDao.xml"/>
</mappers>
</configuration>
这是mybatis核心配置文件,一般放在resources下。与mapper文件前四行不同的地方在于第二行的configuration和第四行的mybatis-3-config,copy过来改一下就行。
根标签是configuration(和第二行对应)。其内的environment标签暂时不知道什么作用,反正在其内配置数据源,老生常谈了。
与environments同级的有个mappers标签,里面通过mapper标签指定mapper文件的位置,resource值为mapper文件的类路径,注意事项‘/’而不是‘.’。有多个mapper文件就要写多个mapper标签
mybatis一般使用tomcat内置的commons-logging。标签内可以改成别的如log4j
@Test
public void test01() throws IOException {
String config = "mybatis.xml";
InputStream stream = Resources.getResourceAsStream(config);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(stream);
SqlSession sqlSession = factory.openSession();
String sqlId="ind.deng.mybatis.dao.BookDao.selectBookById";
Book book = sqlSession.selectOne(sqlId,1);
System.out.println(book);
sqlSession.close();
}
Test方法。先加载mybatis核心配置文件,获取SqlSessionFactory对象并用其openSession获取一个SqlSession对象,用这个对象执行sql语句。感觉还是很繁琐,后边应该有简单的方法(注解?)
selectOne可以只传递sql语句,也可以在后面加个数不定的参数
测试插入
测试插入。此时的占位符有两个及以上的参数,一般接收的是一个对象,所以占位符内的属性名和对应实体类的属性名是一致的。在dao新建方法时,按alt+enter可以快速生成mapper文件中的标签
<insert id="insertBook">
insert into book (name,price) values(#{name},#{price})
</insert>
增删改会修改数据库,而mybatis事务默认提交方式是手动提交。必须加上sqlSession.commit()语句数据库才有变化。其他语句大同小异
@Test
public void test02() throws IOException {
String config = "mybatis.xml";
InputStream stream = Resources.getResourceAsStream(config);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(stream);
SqlSession sqlSession = factory.openSession();
String sqlId="ind.deng.mybatis.dao.BookDao.insertBook";
Book book = new Book();
book.setName("大学物理");
book.setPrice(51.9);
int rows = sqlSession.insert(sqlId,book);
System.out.println(rows);
sqlSession.commit();
sqlSession.close();
}
总结重要对象的作用
Resources:读取配置文件,传给InputStream
SqlSessionFactoryBuilder:根据输入流创建SqlSessionFactory对象
SqlSessionFactory是重量级对象,创建很消耗资源和时间,所以一个项目中有一个就行了。它是一个接口,其实现类是DefaultSqlSessionFactory
SqlSessionFactory中的openSession方法创建SqlSession对象,可以传入bool值,如果为true代表自动提交事务,false则跟不传一样
SqlSession本身是个接口,提供大量的操作sql语句的方法(增删改查(一/全)提交回滚)。DefaultSqlSession是其实现类
SqlSession不是线程安全的,所以需要再一个方法内(局部)使用
使用工具类和模板
添加mapper文件和mybatis核心配置文件的模板
把创建sqlSession的过程封装成工具类:
private static SqlSessionFactory factory = null;
static {
String config = "mybatis.xml";
try {
InputStream stream = Resources.getResourceAsStream(config);
factory = new SqlSessionFactoryBuilder().build(stream);
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession getSqlSession(){
SqlSession session = null;
if(factory!=null){
session = factory.openSession();
}
return session;
}
}
使用工具类:
@Test
public void test03() throws IOException {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
String sqlId="ind.deng.mybatis.dao.BookDao.selectBookById";
Book book = sqlSession.selectOne(sqlId,2);
System.out.println(book);
sqlSession.close();
}
代码量少了一些。当sql语句非常多时,可就不止少了一些了
dao实现类
以上写的代码dao接口似乎没什么用,所以应该为dao接口编写实现类,调用实现类的方法:
public class BookDaoImpl implements BookDao {
@Override
public Book selectBookById(Integer id) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
String sqlId="ind.deng.mybatis.dao.BookDao.selectBookById";
Book book = sqlSession.selectOne(sqlId,id);
return book;
}
@Override
public int insertBook(Book book) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
String sqlId="ind.deng.mybatis.dao.BookDao.insertBook";
int rows = sqlSession.insert(sqlId,book);
System.out.println(rows);
sqlSession.commit();
sqlSession.close();
return rows;
}
}
测试:
@Test
public void test04() {
BookDao bookDao = new BookDaoImpl();
Book book = bookDao.selectBookById(2);
System.out.println(book);
}
@Test
public void test05() {
BookDao bookDao = new BookDaoImpl();
Book book = new Book();
book.setName("亲热天堂");
book.setPrice(99.9);
int rows = bookDao.insertBook(book);
System.out.println(book);
}
dao代理
然而,mybatis根据dao方法的调用,可以得到sqlId(通过反射知道dao接口全限定名,拼接上dao方法便得到sqlId)。然后通过mapper文件的标签得知是增删改查中的那个方法,通过返回值是对象还是集合确定是selectOne还是selectList。
mybatis得知了这些必要信息,所以利用动态代理帮我们在内存中生成dao接口的实现类(代理类)
使用dao代理的前提条件(之前说过):namespace值为dao接口全限定名称;id值为接口对应方法名称
使用:
@Test
public void test04() {
SqlSession session = MyBatisUtil.getSqlSession();
BookDao bookDao = session.getMapper(BookDao.class);
Book book = bookDao.selectBookById(2);
System.out.println(book);
}
@Test
public void test05() {
SqlSession session = MyBatisUtil.getSqlSession();
BookDao bookDao = session.getMapper(BookDao.class);
Book book = new Book();
book.setName("亲热天堂");
book.setPrice(99.9);
int rows = bookDao.insertBook(book);
session.commit();
System.out.println(book);
}
通过session.getMapper(dao接口.class)获得dao接口的对象
parameterType
指定dao方法的形参类型,比如形参类型是Integer,则parameterType=“java.lang.Integer”。但这些值都有别名,java.lang.Integer的别名有int和integer,所以用int即可。mybatis通过反射机制可以知道形参类型,所以parameterType可以不写。只是写了之后可读性更高
dao方法形参
当形参为简单类型(内置类型和String)时,sql语句中用占位符#{}接收参数
有多个简单类型参数时,需要使用@Param注解
dao中的方法:
public List<Book> selectByIdOrPrice(@Param("id") Integer id, @Param("price") Double price);
mapper文件中仍然使用#{}接收参数
<select id="selectByIdOrPrice" resultType="ind.deng.mybatis.entity.Book">
select * from book where id=#{id} or price >#{price}
</select>
测试:
@Test
public void test04() {
SqlSession session = MyBatisUtil.getSqlSession();
BookDao bookDao = session.getMapper(BookDao.class);
List<Book> books = bookDao.selectByIdOrPrice(3,30.0);
books.forEach(book -> System.out.println(book));
}
books.forEach(book -> System.out.println(book));用到了lambda表达式,遍历集合,用和books同类型的book变量接收集合中的每个对象,箭头右边是执行的语句。箭头左边如果有多个参数用()括起来,逗号分隔
这种方法当形参很多时太臃肿,所以一般传对象作为参数,对应的xml文件中#{}中和对象的属性名一致即可。所以我上边的xml文件不需要变化,把dao方法中的形参换为Book book即可
也可以按位置接收形参:id=#{args0} or price >#{args1}
不推荐使用,可读性不好
${}占位符
KaTeX parse error: Expected 'EOF', got '#' at position 10: {}占位符,功能和#̲{}一致,但底层使用state…{}中的内容和sql语句拼接,类似c语言中的宏定义。
效率低且有注入风险。此外,${}就算只有一个普通参数也得用@Param。
但如果想根据传过来的字符串判断按表中哪一列排序,#{}就不好使了,因为会自动给传过来的字符串加上’'比如
select * from book order by ‘id’
这样查出来的数据原封不动,并不会按id排列。要想使得
select * from book order by id
${}就派上用场了,忠实的将传过来的数据拼接
可以在一个sql语句中和#{}同时使用
ResultType
用于select标签中,其值可以是类的全限定名,把查询到的结果集封装为java对象
底层是mybatis利用反射创建该对象,并把查询到的同名列值赋给对象的同名属性。所以数据表字段要和类的属性一一对应
如果dao方法返回值是集合类型,ResultType仍然是对应的对象全类名,然后把查询到的全部对象放入集合中
ResultType也可以表示简单类型。场景:
<select id="selectCount" resultType="java.lang.Integer">
select count(*) from book
</select>
ResultType为Map
public Map<Object,Object> selectMap(@Param("id") Integer id);
<select id="selectMap" resultType="java.util.Map">
select name,price from book where id=#{id}
</select>
查询结果只能是一行数据,将这行数据搜索的属性(name、price)放入map中。多行数据会报错
别名
可以在mybatis主配置文件中为类型定义别名。标签在标签下方
<typeAliases>
<typeAlias type="ind.deng.mybatis.entity.Book" alias="book"/>
</typeAliases>
也可以
<typeAliases>
<package name="ind.deng.mybatis.entity"/>
</typeAliases>
这样这个包内的类的类名就是自己的别名,不区分大小写
不用别名最好
ResultMap
自定义数据表列名和对象属性名的对应关系
<resultMap id="bookMap" type="ind.deng.mybatis.entity.Book">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="price" property="price"/>
</resultMap>
<select id="selectBookById" resultMap="bookMap">
select * from book where id = #{bookId}
</select>
resultMap标签内id对应表中主键,result对应其他字段
column是数据表字段名,property是对象属性名,resultMap就是将两者映射起来的
如果两者值一致,可以不设置
后面的select标签就可以把resultType替换为resultMap。
resultMap可复用,开发中经常使用
resultType也可以解决字段名和属性名不一致的问题,即给列取别名(麻烦)
模糊查询
dao
public List<Book> fuzzySelect(String name);
xml
<select id="fuzzySelect" resultType="ind.deng.mybatis.entity.Book">
select * from book where name like #{name}
</select>
test
@Test
public void test09() {
SqlSession session = MyBatisUtil.getSqlSession();
BookDao bookDao = session.getMapper(BookDao.class);
List<Book> books = bookDao.fuzzySelect("%数%");
books.forEach(book -> System.out.println(book));
session.close();
}
一般在java程序而不是sql语句中组织内容(即%的位置),不然sql频繁改动,不合理
动态sql
同一个dao方法,根据不同条件可以表示不同sql语句,主要是where部分有变化。dao方法的形参是java对象.常用于多条件查询。
if
public List<Book> selectIf(Book book);
if语句内是和对象的属性有关的条件
<select id="selectIf" resultType="ind.deng.mybatis.entity.Book">
select * from book where
<if test="name!=null and name!=''">
name=#{name}
</if>
<if test="price>0">
or price>#{price}
</if>
</select>
可以有多个if,但没有else语句。别忘了加and或or等连接sql语句的连接词。
但有个小细节,如果name为空,sql语句就变成了select * from book where or price>?
这个’or’就很突兀,语法错误
所以可以这样写:
<select id="selectIf" resultType="ind.deng.mybatis.entity.Book">
select * from book where 1!=1
<if test="name!=null and name!=''">
or name=#{name}
</if>
<if test="price>0">
or price>#{price}
</if>
</select>
xml文件中有时候会用大于小于号,和标签符号冲突,应该使用字符实体& l t ; & g t ;
where
if一般和where结合使用
<select id="selectIf" resultType="ind.deng.mybatis.entity.Book">
select * from book
<where>
<if test="name!=null and name!=''">
name=#{name}
</if>
<if test="price>0">
or price>#{price}
</if>
</where>
</select>
if条件有一个满足时,语句后加上where;全都不满足时,不加where
where会删除紧跟在where其后的and或or,因此不会语法错误
foreach
dao
public List<Book> selectForeach(List<Integer> ids);
xml
<select id="selectForeach" resultType="ind.deng.mybatis.entity.Book">
select * from book
<if test="list!=null and list.size > 0">
where id in
<foreach collection="list" open="(" close=")" separator="," item="ids">
#{ids}
</foreach>
</if>
</select>
collection的值,数组为array,list就是list。其他属性见名知意
if语句提高健壮性
foreach遍历对象集合:
public List<Book> selectForeach2(List<Book> books);
<select id="selectForeach2" resultType="ind.deng.mybatis.entity.Book">
select * from book
<if test="list!=null and list.size > 0">
where id in
<foreach collection="list" open="(" close=")" separator="," item="book">
#{book.id}
</foreach>
</if>
</select>
book对应集合中的每个对象,再获得每个对象中的属性值
sql
提取出公共代码片段,在需要的地方引用,实现代码复用
<sql id="selectBook">select * from book</sql>
<select id="selectBookById" resultMap="bookMap">
<include refid="selectBook"></include>
where id = #{bookId}
</select>
感觉很鸡肋,可读性也不好。sql语句原来就不长,复杂的sql也没有多少公共部分
主配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="ind/deng/mybatis/dao/BookDao.xml"/>
</mappers>
</configuration>
settings是mybatis的全局设置,一般默认值即可
用来设置别名,见"别名"内容
是环境标签,可以配置多个环境,一个环境对应一个数据库的连接信息。default取值对应下面环境的id,实现切换不同的环境
id值任意,表示一个环境,一般使用development、online、test等有含义的名字
type还可以取值MANAGED,用于spring,让容器管理事务而不是mybatis
type可以取UNPOOLED(不用数据库连接池)、JNDI(不知道啥东西,现在不用了)
数据库的配置信息可以抽取出来:
jdbc.properties:
driverClass=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/test
username=root
password=root
mybatis主配置文件:
<!--引入properties配置文件-->
<properties resource="jdbc.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<!--用${}接收配置文件中键对应的值-->
<property name="driver" value="${driverClass}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
用来指定mapper文件的位置
属性为resource时,值为xml文件的全限定名,精确,但数量多时写起来麻烦
属性为name时,只需给出包名(一般都在dao下),该包下的所有xml文件都会被确定。前提条件:mapper文件和对应的dao接口在同一目录下;mapper文件和dao接口名称一致
PageHelper分页插件
先引入依赖:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.0</version>
</dependency>
在mybatis主配置文件中之前加入
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
测试:
@Test
public void test06() {
SqlSession session = MyBatisUtil.getSqlSession();
BookDao bookDao = session.getMapper(BookDao.class);
PageHelper.startPage(1,3);
List<Book> books = bookDao.selectBooksOrderByColumnName("id");
books.forEach(book -> System.out.println(book));
session.close();
}
PageHelper.startPage(1,3);第一个参数对应当前显示第几页,从1开始;第二个参数对应每页的记录数