myBatis
mybatis中文参考文档地址:
1.持久化
数据持久化:
- 持久化就是将程序在持久状态与瞬时状态转化的过程
- 数据库(jdbc),io
持久化的目的:
- 有一些对象不能丢失
- 降低内存的使用量
2.第一个mybatis程序
2.1搭建数据库环境
CREATE DATABASE `mybatis`;USE mybatis;CREATE TABLE `user`(`id` INT(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',`name` VARCHAR(10) NOT NULL COMMENT '用户姓名',`pwd` VARCHAR(15) NOT NULL COMMENT '用户密码',PRIMARY KEY(`id`))ENGINE=INNODB DEFAULT CHARSET=utf8;INSERT INTO `user` (`name`,`pwd`)VALUES ('张三',123456789),('李四',987654321),('王五',456789123),('赵六',789123456);
2.2创建一个maven父项目(管理jar包)
2.3创建一个模块myBatis-01
2.4配置myBatis核心文件
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configurationPUBLIC "-//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="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql:///jdbcstudy?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=UTC"/><!--下面这样的是错误的--><!--<property name="url" value="jdbc:mysql://localhost:3306/jdbcstudy?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"/> --><property name="username" value="root"/><property name="password" value="123456"/></dataSource></environment></environments><mappers><mapper resource="org/mybatis/example/BlogMapper.xml"/></mappers></configuration>
2.4编写myBatis工具类
package com.xiao.utils;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;import java.io.InputStream;/*** 用于获取操作sql语句的对象*/public class MyBatisUtils {private static InputStream inputStream = null;private static SqlSessionFactory sqlSessionFactory = null;/*** 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。* SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。* 而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的* Configuration 实例来构建出 SqlSessionFactory 实例。*/static {try {String resource = "myBatis-config.xml";inputStream = Resources.getResourceAsStream(resource);sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);} catch (IOException e) {e.printStackTrace();}}/*** 返回的SqlSession实际上相当于jdbc里面的Statement或PreparedStatement,用于执行sql语句* @return SqlSession*/public static SqlSession getSqlSession(){return sqlSessionFactory.openSession();}}
2.5代码
- 实体类
package com.xiao.pojo;/*** 实体类*/public class User {private int id;private String name;private String pwd;public User() {}public User(int id, String name, String pwd) {this.id = id;this.name = name;this.pwd = pwd;}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getPwd() {return pwd;}public void setPwd(String pwd) {this.pwd = pwd;}}
- 接口类
package com.xiao.dao;import com.xiao.pojo.User;import java.util.List;public interface UserList {public List<User> getUserList();}
- 包含sql语句的xml文件(相当于接口实现类)
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--与接口类绑定--><mapper namespace="com.xiao.dao.UserList"><!-- id是对应了接口类的获取查询结果的方法 resultType是对应了查询返回的对象--><select id="getUserList" resultType="com.xiao.pojo.User">select * from `mybatis`.`user`</select></mapper>
2.6测试
三个常见异常
org.apache.ibatis.binding.BindingException: Type interface com.xiao.mapper.UserMapper is not known to the MapperRegistry.绑定异常,每一个包含了sql语句的xml文件都必须在mybatis核心xml文件中用以下标签注册。
<mappers><mapper resource="com/xiao/mapper/UserMapper.xml"/></mappers>
java.lang.ExceptionInInitializerError资源获取异常(这是maven的问题,maven默认的资源获取位置在resources文件夹中,但在mybatis中经常会将sql的xml文件放在java的文件夹下,这样就会导致资源无法获取,因此要在pom.xml文件中手动配置资源过滤)
这一段xml表示maven项目开启了在src/main/java文件夹与src/main/resources文件夹(默认开启)的过滤,即从里面获取properties与xml资源
这一段xml配置比较常用
<build><resources><resource><directory>src/main/java</directory><includes><include>**/*.properties</include><include>**/*.xml</include></includes><filtering>false</filtering></resource><resource><directory>src/main/resources</directory><includes><include>**/*.properties</include><include>**/*.xml</include></includes><filtering>false</filtering></resource></resources></build>
org.apache.ibatis.exceptions.PersistenceException:
Error querying database. Cause: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
数据库连接错误。
8.0+版本的jdbc在mybatis下的数据库url的格式是这样的:(注意,要有三条斜杠,而且默认连接127.0.0.1:3306,不用指明localhost:3306)
"jdbc:mysql:///jdbcstudy?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=UTC"
package com.xiao.mapper;import com.xiao.pojo.User;import com.xiao.utils.MyBatisUtils;import org.apache.ibatis.session.SqlSession;import org.junit.Test;import java.util.List;public class UserMapperTest {@Testpublic void test(){/*** 获取执行sql语句的对象*/SqlSession sqlSession = MyBatisUtils.getSqlSession();/*** 由于要执行的是一条select语句,所以会返回结果集,存储在userList中*/UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> userList = mapper.getUserList();for(User user:userList){System.out.println(user);}/*** 关闭资源*/sqlSession.close();}}
2.7总结
大致过程:
- 首先要配置mybatis-config.xml,里面包含了数据库的连接信息(驱动设备,数据库url,用户名,密码),这样就具备了连接数据库的前提,
- 接着在utils包下写一个工具类,这个工具类就是完成得到一个sqlSession的工作(首先Rsources类获取资源,返回一个InputStream,接着用返回的数据库资源在sqlSessionFactoryBuilder中build产生一个sqlSessionFactory,最后调用sqlSessionFactory的openSession方法返回sqlSession对象)
- 现在,数据库已经成功连接,需要一个(或多个)与指定数据库对应的实体类(如数据库保存的是用户信息,就要创建一个用户类与数据段对应)
- 接着就要有一个接口(比如UserMapper)实现对对应数据字段(实体类实例)的操作[比如要用select语句查询就需要有返回一个实体类对象或对象列表的方法]
- 然后就是在包含sql(即sql语句)的xml文档中配置信息。当然,这个xml文档中首先要有一个命名空间与上面定义的接口绑定(相当于实现该接口),然后就是sql语句了,比如我们需要一个selece的sql语句,这个标签有两个重要属性,一个是id(对应接口中的一个方法),一个是resultType(或resultMap等)对应查询返回的对象.这样这个xml就配置好了
- 最后就是执行sql语句,首先对于当前进程,应当先通过工具类获取一个sqlsession实例,这里由于要执行的是一条select语句,所以会返回结果集,存储在userList中。
- 现在查询的对象就存储在列表中了。
3.CRUD
1.namespace(命名空间)
namespace必须与mapper接口名保持一致
2.select
选择,查询语句
- id: 就是namespace对于接口的一个方法名
- resultType: select语句返回的对象类型(即实体类)
- parameterType : 调用的方法的参数类型(如int,String)
1.首先在接口中写一个对应方法
public List<User> getUserList();public User getUserListByID(int id);
2.在xml文件中写sql语句
<select id="getUserList" resultType="com.xiao.pojo.User">select * from mybatis.user</select><select id="getUserListByID" parameterType="int" resultType="com.xiao.pojo.User">select * from mybatis.user where id = #{id}</select>
3.测试
@Testpublic void testSelect(){/*** 获取执行sql语句的对象*/SqlSession sqlSession = MyBatisUtils.getSqlSession();/*** 由于要执行的是一条select语句,所以会返回结果集,存储在userList中*/try {UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> userList = mapper.getUserList();for(User user:userList){System.out.println(user);}}catch ( Exception e){e.printStackTrace();}//实际上这里的catch没有实际意义(只是为了在finally中关闭资源)finally {/*** 关闭资源*/sqlSession.close();}}@Testpublic void testSelect1(){SqlSession session = MyBatisUtils.getSqlSession();UserMapper mapper = session.getMapper(UserMapper.class);User userListByID = mapper.getUserListByID(1);System.out.println(userListByID);session.close();}
3.insert
1.首先在接口中写一个对应方法
public void insertUser(User user);
2.在xml文件中写sql语句
<insert id = "insertUser" parameterType="com.xiao.pojo.User">insert into mybatis.user (name, pwd) values(#{name},#{pwd})</insert>
3.测试
public void testInsert(){SqlSession session = MyBatisUtils.getSqlSession();UserMapper mapper = session.getMapper(UserMapper.class);mapper.insertUser( new User(5,"小红","7539514682"));session.commit(); /*不同于select,删,该,查都应该有事务提交(commit)操作*/session.close();}
4.delete
1.首先在接口中写一个对应方法
public void deleteUserByName(String name);
2.在xml文件中写sql语句
<delete id = "deleteUserByName" parameterType="String">delete from mybatis.user where name = #{name}</delete>
3.测试
@Testpublic void testDelete(){SqlSession sqlSession = MyBatisUtils.getSqlSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);mapper.deleteUserByName("小红");sqlSession.commit();sqlSession.close();}
5.update
1.首先在接口中写一个对应方法
public void updatePwdByName(User user);
2.在xml文件中写sql语句
<update id = "updatePwdByName" parameterType="com.xiao.pojo.User">update mybatis.user set pwd = #{pwd} where name = #{name}</update>
3.测试
@Testpublic void testUpdate(){SqlSession sqlSession = MyBatisUtils.getSqlSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);mapper.updatePwdByName(new User("张三","47895123456"));sqlSession.commit();sqlSession.close();}
4.map
在上面的update中,可以发现:传统的方法参数类型必须是对象
(parameterType不能包含两个参数,但update语句通常有set需要一个或多个参数,where判断又需要一个或多个参数,这就导致必须创建对象,如果对象的构造参数比较少还好,一旦对象的参数十分多,而又只修改一个值,就会显得极为繁琐),
所以我们想要有一种参数可以随意包含任意多的字段,我们可以想到,虽然是要对数据库的一个对象操作,但我们实际上不一定要传递完整的对象,我们需要的只是对象的属性而已,比如我们通过name去修改某个人的id,那我们完全不需要创建对象,我们只需要name与id而已,
为了解决这个问题,首先考虑为什么可以用实体类的对象作为参数呢?因为这个对象就包含了我们需要的所有字段,而每个字段又对应了一个值,比如我在update语句中传入User对象,那么在update语句中我就可以直接通过这个对象的属性去锁定一个值(如update mybatis.user set id = #{id} where name = #{name} 里面的id与name就是对象的属性),所以对象实际上就是包含了n个键值对的玩意儿,只是由于对象构造的问题,导致我们需要参入的值的数目可能会不可控,
所以我们就想到了另一种键值对(天然的键值对map),通过map,我们不仅可以随意掌控键值对的数目,还可以让键有别名(比如我要去修改id这个属性,如果是类对象的话,格式就必须是set id = #{id},但在map中可以是set id = #{personID},只要personID对应的值时我想要修改的值就好)
1.首先在接口中写一个对应方法
public void updatePwdByName(Map<String,Object> map);
2.在xml文件中写sql语句
<update id = "updatePwdByName" parameterType="map">update mybatis.user set pwd = #{newpwd} where name = #{targetname}</update>
3.测试
@Testpublic void testUpdate2(){SqlSession sqlSession = MyBatisUtils.getSqlSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);HashMap<String, Object> map= new HashMap<>();map.put("targetname","王五");map.put("newpwd",123456789);/*这里map里面就可以有随意的属性*/mapper.updatePwdByName2(map);sqlSession.commit();sqlSession.close();}
注意即使接口中的两个同名方法参数不相同,只要xml文档中出现了两个相同的 id就会报错
5.配置解析
1.核心配置文件(mybatis-config.xml)[当然也可以是其他的名字]
MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:
configuration(配置):
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
properties(属性)
这些属性可以在外部进行配置,并可以进行动态替换。你既可以在典型的 Java 属性文件中配置这些属性,也可以在 properties 元素的子元素中设置。例如:
driver = com.mysql.cj.jdbc.Driverurl = jdbc:mysql:///mybatis?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaiusername = rootpassword = 123456
设置好的属性可以在整个配置文件中用来替换需要动态配置的属性值。比如:
<properties resource="db.properties" /> <!--获取配置文件中的所有信息--><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>
这个例子中的 username , password , driver 和 url 属性将会由 config.properties 文件中对应的值来替换。
上面的例子是将所有的properties 在properties 文件中配置好后获取,当然也可以只在properties文件里面配置一部分,另一部分在properties的属性标签里面写,例如:
driver = com.mysql.cj.jdbc.Driverurl = jdbc:mysql:///mybatis?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
<configuration><properties resource="db.properties" ><property name="username" value="root"/><property name="password" value="123456"/></properties><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>
这个例子中的 username 和 password 将会由 properties 元素中设置的相应值来替换。 driver 和 url 属性将会由 config.properties 文件中对应的值来替换。
如果一个属性在不只一个地方进行了配置,那么,MyBatis 将按照下面的顺序来加载:
- 首先读取在 properties 元素体内指定的属性。
- 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。
- 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。
environments(环境配置)
由environments是复数就可以看出实际上是可以配置多套环境的
<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><!--下面这套环境也会对应一个数据库--><environment id="test"><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>
那么如何判断要连接那个数据库呢?
<environments default="development">
由default决定,程序最终只会连接default的数据库
transactionManager(事务管理器)
在 MyBatis 中有两种类型的事务管理器(也就是 type=”[JDBC|MANAGED]”):
- JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。一般默认使用
- MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为.
dataSource(数据源)
dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。
- 大多数 MyBatis应用程序会按示例中的例子来配置数据源。虽然数据源配置是可选的,但如果要启用延迟加载特性,就必须配置数据源。
有三种内建的数据源类型(也就是 type=”[UNPOOLED|POOLED|JNDI]”):
UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。
POOLED– 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。一般默认使用
JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
typeAliases(类型别名)
类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。
1.直接起别名
例如:
<typeAliases><typeAlias type="com.xiao.entity.User" alias="user"/></typeAliases>
当这样配置时,user 可以用在任何使用com.xiao.entity.User 的地方。(比如UserMapper.xml中的返回值类型)
未起别名:
<select id="getUserList" resultType="com.xiao.entity.User">select * from mybatis.user</select><select id="getUserListByID" parameterType="int" resultType="com.xiao.entity.User">select * from mybatis.user where id = #{id}</select><insert id = "insertUser" parameterType="com.xiao.entity.User">insert into mybatis.user (name, pwd) values(#{name},#{pwd})</insert>
起别名后:
<select id="getUserList" resultType="user">select * from mybatis.user</select><select id="getUserListByID" parameterType="int" resultType="user">select * from mybatis.user where id = #{id}</select><insert id = "insertUser" parameterType="user">insert into mybatis.user (name, pwd) values(#{name},#{pwd})</insert>
2.通过包名间接起别名
也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean(实体类),比如:
<typeAliases><package name="com.xiao.entity"/></typeAliases>
在没有注解的情况下,会把指定包下实体类的类名的首字母小写版作为别名(上面的例子中就会把com.xiao.entity包下的User对应的user作为别名)
3.通过注解起别名
import org.apache.ibatis.type.Alias;/*** 实体类*/@Alias("user")public class User {......}
若有注解,则别名为其注解值.
6.映射器mapper
在上面的学习中可以知道,每个实体类(entiet Class)都必须有一个对应的mapper接口与xml文件用来执行对应的SQL语句,并且为了方便起见,这个接口与xml问价会放在一个包里面,即mapper包下,这节导致了两个问题
- xml文件不在resources文件夹下面,默认情况下maven找不到,所以要在maven的pom.xml中设置资源过滤。
- mybatis需要我们为每一个这样的xml文件注册。
这两个问题在上面已经解决了,现在有一个新的问题:在mabatis-config.xml中注册时只有这一种方法吗?\
1.方法一:硬性指定xml资源位置(推荐方法)
<!--每一个Mapper.xml 都必须在mybatis核心配置文件中注册--><mappers><mapper resource="com/xiao/mapper/UserMapper.xml"/></mappers>
方法二:通过mapper接口间接定位
<!--每一个Mapper.xml 都必须在mybatis核心配置文件中注册--><mappers><mapper class="com.xiao.mapper.UserMapper"/></mappers>
这个方法有两个注意点:
- 由于指定的是一个接口名(UserMapper),所以xml文件名必须与接口名一致,即(UserMapper.xml)\
- xml文件必须与接口类在同一个包下面
还有一种通过包来定位,但使用较少。
7.ResultMap(重点)
首先像一个问题:从一开始我们创建实体类(为什么叫实体类,实际上这个类是与要查询的数据库的表项对应的),我们就把实体类的属性名与对应表项的字段名设置为一样的,比如id INT(10) 就对应private int id,但是可不可以是不一样的呢,比如把实体类User的pwd该名为password,会不会有问题呢?
就向这样(注意get与set方法名也必须改)
package com.xiao.entity;import org.apache.ibatis.type.Alias;/*** 实体类*/@Alias("user")public class User {private int id;private String name;private String pasword;public User() {}public User(int id, String name, String pwd) {this.id = id;this.name = name;this.pasword = pwd;}public User(String name, String pwd) {this.name = name;this.pasword = pwd;}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getPasword() {return pasword;}public void setPasword(String pasword) {this.pasword = pasword;}@Overridepublic String toString() {return "User{" +"id=" + id +", name='" + name + '\'' +", pwd='" + pasword + '\'' +'}';}}
尝试去查询
package com.xiao;import com.xiao.entity.User;import com.xiao.mapper.UserMapper;import com.xiao.utils.MyBatisUtils;import org.apache.ibatis.session.SqlSession;import org.junit.Test;public class UserMapperTest {@Testpublic void testgetUserListByID(){SqlSession sqlSession = MyBatisUtils.getSqlSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);User user = mapper.getUserListByID(2);System.out.println(user);sqlSession.close();}}/*结果*/User{id=2, name='李四', pwd='null'}/*显然pwd这个字段没有查出来,为什么呢select * from mybatis.user where id = #{id}实际上就相当于:select id, name, pwd from mybatis.user where id = #{id}*/
首先,为什么要保证set与get方法名要与属性保持一致,这一点非常重要,为什么呢,只要使用的对象是实体类的对象,比如User,那所有的SQL语句实际上不是去直接使用对象的属性,而是优先使用getter与setter,只有当set或get不到属性时,才会尝试去直接获取属性,
比如用select语句返回一个User,那就会创建一个新的对象,属性为空,然后按照查询的结果调用set方法给属性赋值,然后返回。
如果用insert语句,首先当然需要创建一个对象,要插入式时,就调用对象的set方法获取到要查询的属性,然后插入
delete,update也是一样的道理。
本质原因是什么呢,不论是什么SQL语句,操作而永远只是字段,属性只是映射,由于属性是私有的,所以才需要geter与setter来获取。(虽然set或get不到属性时,会尝试获取对象的属性值,但直接获取对象的私有属性值是”不合法”的)
这就想到之前为了解决有时只需要用到几个字段而实体类对象有较多属性的例子,我们用到了map(hasnMap),不同于实体类对象,map里面只有键与值的对应,相当于字段与值的对应,可以直接通过键来获取值,键只是跳板,我们需要的只是确保程序可以正确的定位到键,这也是为什么用map时键可以随便起名的原因.
我们可以验证一下:(我要插入一个对象,按照上面的理论,对象的属性名无所谓,只需要sql语句可以正确的找到一个值就可以)
这是接口的方法
public void insertUser(User user);
这是xml文件中的SQL语句(sql语句里面写明首先产的参数是User对象,我要的是这个对象的name属性与password属性,为此,我们需要有方法名为getName与getPassword这两个方法,如果没有就会有问题,那我们就让属性名为pasword但是方法名为getPwd),现在getter与属性名都与需要的password不一致,理论上就是获取不到的
<insert id = "insertUser" parameterType="user">insert into mybatis.user (name, pwd) values ( #{name}, #{password})</insert>
程序报错:(异常中表明没有getter)
Error updating database. Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named ‘password’ in ‘class com.xiao.entity.User’
那么如果想要让这条插入语句成功,就应该修改方法名为getPassword:
public String getPassword() {return pasword;}
但不修改属性名:
private String pasword;
插入成功!
看起来我们解决了当实体类对象属性名与字段名不对应的问题,那就是只要保证方法名可以对应上就可以了(特指setter与getter)
但这样无论怎么看都是不合适的,我们理应保证setter与getter与属性名保持一致(这是java的规范),那这时我们该怎么解决这个问题呢?那就是ResultMap(它是之前使用的ResultTyoe的”升级版”)
这是mybatis官方的原话:
结果映射(基础)
`resultMap` 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC `ResultSets` 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 `resultMap` 能够代替实现同等功能的数千行代码。ResultMap的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
用上面的查询的案例举例:
<resultMap id="userList" type="user"><result column="pwd" property="password"/><result column="id" property="id"/><result column="name" property="name"/></resultMap><select id="getUserList" resultMap="userList">select * from mybatis.user</select>
解释:
修改resultType为resultMap,resultMap在select标签里面实际上充当一个id的角色,需要额外的resultMap标签来描述,
在resultMap标签里面id就是锁定了一个包含resultMap的sql语句标签,type表示实体类对象(这里用了别名),接下来就是想办法让字段名与属性名对应了,这一工作在result子标签里面实现,column=”pwd”表示字段名为pwd,property=”password”表明属性名为password,这个标签就实现了pwd到password的映射(程序在数据库里面找到的pwd字段全部会被看做password,这样就可以调用setPassword方法了,而且查询的结果会保留pwd这一名字)
这是查询结果:
User{id=1, name='张三', pwd='47895123456'}User{id=2, name='李四', pwd='987654321'}User{id=3, name='王五', pwd='123456789'}User{id=4, name='赵六', pwd='789123456'}User{id=6, name='小明', pwd='1654986'}
实际上除了这种方法,我们还有一种==“幼儿园做法”-==——-为字段起别名,就向这样:
<select id="getUserList" resultType="user">select id, name, pwd as password from mybatis.user</select>
8.日志
8.1日志工厂
之前的java程序里面,我们可以用sout与debug排除错误,日志实际上也是这个作用
mybatis的设置里面有一项设置了日志工厂
| 设置名 | 描述 | 有效值 | 默认值 |
|---|---|---|---|
| logImpl | 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 | SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING | 未设置 |
其中STDOUT_LOGGING与LOG4J重要
- SLF4J
- LOG4J 【重要】
- LOG4J2
- JDK_LOGGING
- COMMONS_LOGGING
- STDOUT_LOGGING 【重要,是mybatis的默认自带的日志工厂(但是不会默认开启)】
- NO_LOGGING (这个是没有日志)
来测试一下日志的使用:这是测试的方法
@Testpublic void testgetUserListByID(){SqlSession sqlSession = MyBatisUtils.getSqlSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);User user = mapper.getUserListByID(1);System.out.println(user);sqlSession.close();}
首先我们先不去配置日志工厂,结果如下:
User{id=1, name='张三', pwd='47895123456'}
接着我们先用 STDOUT_LOGGING尝试一下,这是配置
<settings><setting name="logImpl" value="STDOUT_LOGGING"/></settings>
这是结果:
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.Class not found: org.jboss.vfs.VFSJBoss 6 VFS API is not available in this environment.Class not found: org.jboss.vfs.VirtualFileVFS implementation org.apache.ibatis.io.JBoss6VFS is not valid in this environment.Using VFS adapter org.apache.ibatis.io.DefaultVFSFind JAR URL: file:/G:/java/myBatisLearn/myBatis_04/target/classes/com/xiao/entityNot a JAR: file:/G:/java/myBatisLearn/myBatis_04/target/classes/com/xiao/entityReader entry: User.classListing file:/G:/java/myBatisLearn/myBatis_04/target/classes/com/xiao/entityFind JAR URL: file:/G:/java/myBatisLearn/myBatis_04/target/classes/com/xiao/entity/User.classNot a JAR: file:/G:/java/myBatisLearn/myBatis_04/target/classes/com/xiao/entity/User.classReader entry: ���� 1 BChecking to see if class com.xiao.entity.User matches criteria [is assignable to Object]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 206835546./*设置自动提交为false*/Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@c540f5a]/*预编译*/==> Preparing: select * from mybatis.user where id = ?==> Parameters: 1(Integer)<== Columns: id, name, pwd<== Row: 1, 张三, 47895123456<== Total: 1/*查询结果*/User{id=1, name='张三', pwd='47895123456'}Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@c540f5a]Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@c540f5a]Returned connection 206835546 to pool.
显然在日志工厂的作用下,程序的执行过程是十分详细
8.2Log4J
如果直接设置日志工厂为Log4J,会报错,因为Log4J不是默认自带的,需要我们自己导入
Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件等。
- 我们也可以控制每一条日志的输出格式。
- 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
- 这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
这是log4j的依赖(log4j现在已经比较完善了,一般不会再有新版本)
<!-- https://mvnrepository.com/artifact/log4j/log4j --><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency>
这是配置文件:
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码log4j.rootLogger=DEBUG,console,file#控制台输出的相关设置log4j.appender.console = org.apache.log4j.ConsoleAppenderlog4j.appender.console.Target = System.outlog4j.appender.console.Threshold=DEBUGlog4j.appender.console.layout = org.apache.log4j.PatternLayoutlog4j.appender.console.layout.ConversionPattern=[%c]-%m%n#文件输出的相关设置log4j.appender.file = org.apache.log4j.RollingFileAppender#下面这句配置表示每次的日志信息都会记录在log/xiao.log这个文件中log4j.appender.file.File=./log/xiao.loglog4j.appender.file.MaxFileSize=10mblog4j.appender.file.Threshold=DEBUGlog4j.appender.file.layout=org.apache.log4j.PatternLayoutlog4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n#日志输出级别log4j.logger.org.mybatis=DEBUGlog4j.logger.java.sql=DEBUGlog4j.logger.java.sql.Statement=DEBUGlog4j.logger.java.sql.ResultSet=DEBUGlog4j.logger.java.sql.PreparedStatement=DEBUG
简单使用:
要使用Log4j,首先要导入包
import org.apache.log4j.Logger;
因为可能有多个方法会用到,所以创建属性
public class UserMapperTest {/*一般要是static修饰*/static Logger logger = Logger.getLogger(UserMapperTest.class);......}
结果:
[com.xiao.UserMapperTest]-info:logger进入info[com.xiao.UserMapperTest]-debug:logger进入debug[com.xiao.UserMapperTest]-error:logger进入error
不仅如此,这里的每一条记录都会在指定的文件里面有记录(这样,就可以通过分析日志文件来排错)
9.分页
使用limit实现分页
传统的分页就是通过limit实现的
1.定义接口方法:
public List<User> getUserListByLimit(Map<String,Integer> map);
2.配置xml文件:
<select id="getUserListByLimit" parameterType="map" resultType="User">select * from mybatis.user limit #{startIndex},#{pageSize}</select>
3.测试:
@Testpublic void testGetUserListByLimit(){SqlSession sqlSession = MyBatisUtils.getSqlSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);HashMap<String, Integer> map = new HashMap<String, Integer>();map.put("startIndex",0);map.put("pageSize",2);List<User> userList = mapper.getUserListByLimit(map);userList.forEach(user->System.out.println(user));sqlSession.close();}
4.结果:
User{id=1, name='张三', pwd='null'}User{id=2, name='李四', pwd='null'}
注意:这里我使用了Lambda表达式,而使用Lambda表达式需要确保Language Level >= 8(java8之后才有Lambda表达式),
此外,还需要在pom.xml中配置
<build><plugins><plugin><artifactId>maven-compiler-plugin</artifactId><version>3.1</version> <!--这里的版本是compiler插件的版本--><configuration><source>1.8</source><target>1.8</target></configuration></plugin></plugins></build>
使用RowBounds实现分页(用的较少)
之前的limit分页是基于sql层面实现的分页,而RowBounds是在java层面实现分页
1.定义接口方法
/*** 不同于limit实现分页,RowBounds不需要传参数、* @return List<User>*/public List<User> getUserListByRowBounds();
2.配置xml文件
<!--基于RowBounds的分页下SQL语句只管查询所有--><select id="getUserListByRowBounds" resultType="user">select * from mybatis.user</select>
3.测试(这里面的selectList方法实际上也是mybatis官方不推荐使用的)
@Testpublic void testGetUserListByRowBounds(){SqlSession sqlSession = MyBatisUtils.getSqlSession();/**RowBounds rowBounds = new RowBounds(0, 2);里面的两个参数:开始索引,页面大小(与limit一致)*/RowBounds rowBounds = new RowBounds(0, 2);List<User> user = sqlSession.selectList("com.xiao.mapper.UserMapper.getUserListByRowBounds",null, rowBounds);user.forEach(user1 -> System.out.println(user1));sqlSession.close();}
4.结果(与上面的是一样的)
User{id=1, name='张三', pwd='null'}User{id=2, name='李四', pwd='null'}
/*这是RowBounds的一个有参构造器*/public RowBounds(int offset, int limit) {this.offset = offset;this.limit = limit;}
10.使用注解
注解在这里的作用实际上就是代替了xml文档里的sql语句的相关配置,将sql语句写在注解里面
public interface UserMapper {/*** 查询所有user* @return List<User>*/@Select("select * from user")public List<User> getUserList();}
修改mapper注册信息:(使用class注册后,只要xml文件与接口在同一个包下面而且名字相同,就可以同时完成注册)
<!--虽然现在使用注解,仍然需要注册,可以用class--><mappers><mapper class="com.xiao.mapper.UserMapper"/></mappers>
测试:
@Testpublic void testSelect(){/*** 获取执行sql语句的对象*/SqlSession sqlSession = MyBatisUtils.getSqlSession();/*** 由于要执行的是一条select语句,所以会返回结果集,存储在userList中*/try {UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> userList = mapper.getUserList();for(User user:userList){System.out.println(user);}}catch ( Exception e){e.printStackTrace();}//实际上这里的catch没有实际意义(只是为了在finally中关闭资源)finally {/*** 关闭资源*/sqlSession.close();}}
也可以完成查询,但是注解有局限性
这是官方文档的原话:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。选择何种方式来配置映射,以及认为是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 换句话说,永远不要拘泥于一种方式,你可以很轻松的在基于注解和 XML的语句映射方式间自由移植和切换。
比如上面的ResultMap的使用,在注解里面就是不可能的
使用注解实现CRUD与Param注解
接口类(CRUD四个方法):
public interface UserMapper {/*** 查询所有user* @return List<User>*/@Select("select * from user")public List<User> getUserList();/*** 删除用户* @param id 查询id* @param name 查询姓名*/@Delete("delete from user where id = #{userId} and name = #{userName}")public void deleteUserByNameAndId(@Param("userId") int id, @Param("userName") String name);/*** 插入用户* @param user 要插入的用户*/@Insert("insert into user(id,name,pwd) values(#{id},#{name},#{password})")public void insertUser(User user);/*** 修改制定用户的密码* @param name 查询姓名* @param newPwd 新的密码*/@Update("update user set pwd = #{newPwd} where name = #{name}")public void updateUserByName(@Param("name") String name ,@Param("newPwd") String newPwd );}
注册:
<mappers><mapper class="com.xiao.mapper.UserMapper"/></mappers>
测试:
public class UserMapperTest {@Testpublic void testSelect(){/*** 获取执行sql语句的对象*/SqlSession sqlSession = MyBatisUtils.getSqlSession();/*** 由于要执行的是一条select语句,所以会返回结果集,存储在userList中*/try {UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> userList = mapper.getUserList();for(User user:userList){System.out.println(user);}}catch ( Exception e){e.printStackTrace();}//实际上这里的catch没有实际意义(只是为了在finally中关闭资源)finally {/*** 关闭资源*/sqlSession.close();}}@Testpublic void testDeleteUserByNameAndId(){SqlSession sqlSession = MyBatisUtils.getSqlSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);mapper.deleteUserByNameAndId(6,"小明");mapper.deleteUserByNameAndId(7,"小明");mapper.deleteUserByNameAndId(8,"小明");sqlSession.commit();sqlSession.close();}@Testpublic void testInsertUser(){SqlSession sqlSession = MyBatisUtils.getSqlSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);mapper.insertUser(new User(20,"大福","161261551"));sqlSession.commit();sqlSession.close();}@Testpublic void testUpdateUserByName(){SqlSession sqlSession = MyBatisUtils.getSqlSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);mapper.updateUserByName("大福","111111111");sqlSession.commit();sqlSession.close();}}
关于@Param()注解
- 基本数据类型与String的参数要加上。
- 引用类型的参数不用加(比如User的对象)。
- 方法有多个参数时必须加上。
- 方法只有一个参数是可以不加,但最好加上。
小知识点:#{}与${}
在mybatis里面,我们一直用的是#{},比如 where id = #{id} ,但是实际上where id =
{}不能。
11.多对一 的处理
搭建一个多对一的数据库环境:
CREATE TABLE `teacher` (`id` INT(10) NOT NULL,`name` VARCHAR(30) DEFAULT NULL,PRIMARY KEY (`id`)) ENGINE=INNODB DEFAULT CHARSET=utf8INSERT INTO teacher(`id`, `name`) VALUES (1, '秦老师');CREATE TABLE `student` (`id` INT(10) NOT NULL,`name` VARCHAR(30) DEFAULT NULL,`tid` INT(10) DEFAULT NULL,PRIMARY KEY (`id`),KEY `fktid` (`tid`),CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`)) ENGINE=INNODB DEFAULT CHARSET=utf8 ;INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('1', '小明', '1');INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('2', '小红', '1');INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('3', '小张', '1');INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('4', '小李', '1');INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('5', '小王', '1');
建立了两个数据库:一个学生的表,一个老师的表,每个学生都有一个tid与老师的id对应
现在有一个问题:查询所有的学生信息,其中包含老师的信息.
在SQL层面很容易实现
SELECT student.id, student.`name`,tid,teacher.`name` FROM student , teacher WHERE student.`tid`=teacher.`id`;
但是在mybatis中怎么实现呢?
先建立实体类Student与Teacher
package com.xiao.entity;import org.apache.ibatis.type.Alias;@Alias("student")public class Student {private int id;private String name;//一个学生有一个老师private Teacher teacher;public Student(int id, String name, Teacher teacher) {this.id = id;this.name = name;this.teacher = teacher;}public Student() {}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Teacher getTeacher() {return teacher;}public void setTeacher(Teacher teacher) {this.teacher = teacher;}@Overridepublic String toString() {return "Student{" +"id=" + id +", name='" + name + '\'' +", teacher=" + teacher +'}';}}
package com.xiao.entity;import org.apache.ibatis.type.Alias;@Alias("teacher")public class Teacher {private int id;private String name;public Teacher(int id, String name) {this.id = id;this.name = name;}public Teacher() {}public int getId() {return id;}public String getName() {return name;}public void setId(int id) {this.id = id;}public void setName(String name) {this.name = name;}@Overridepublic String toString() {return "Teacher{" +"id=" + id +", name='" + name + '\'' +'}';}}
创建接口:
package com.xiao.mapper;import com.xiao.entity.Student;import java.util.List;public interface StudentMapper {public List<Student> getStudent();}
package com.xiao.mapper;import com.xiao.entity.Teacher;public interface TeacherMapper {public Teacher getTeacher();}
第一种方式【在标签中建立连接】(类似于子查询)
首先,我们知道,学生的teacher属性是一个对象,我们要查询的话,学生与老师必须要有对应关系(要是没有,就不会连表查询了),第一种查发的思想是:首先我们直接查询学生的所有信息,接着我们通过学生的tid作为查询条件去查询teacher,所以为什么类似于子查询
问题的关键在于StudentMapper.xml的配置
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--与接口类绑定--><mapper namespace="com.xiao.mapper.StudentMapper"><select id="getStudent" resultMap="student_teacher">select * from student</select><resultMap id="student_teacher" type="student"><result property="id" column="id"/><result property="name" column="name"/><association property="teacher" javaType="teacher" column="tid" select="getTeacher"/></resultMap><select id="getTeacher" resultType="teacher">select * from teacher where id = #{tid}</select></mapper>
这上面配置的关键是resultMap与assoociation的使用:
- 首先属性中有个对象,肯定不能直接用resultType,所以用resultMap(当然可以随便起名),
- 在resultMap配置属性与字段的对应中id与name没有问题
- 而teacher属性显然不能直接对应,
- 首先association标签是关联的意思,在多对一(这个情况下一个学生就对应一个老师,即关联)里面,就使用association标签表示一个对象的某个属性与另一个对象关联
- 如何创建关联呢?
- property=”teacher”表示属性是teacher这个对象
- javaType=”teacher”表示要查询的关联对象是是teacher(用了别名)
- column=”tid”表示这一列是查询条件
- select=”getTeacher”是跳板,表明了查询的方式
- select * from teacher where id = #{tid}表示就用传来的tid这个值来查询teacher,并把这个teacher作为student的属性
结果:
Student{id=1, name='小明', teacher=Teacher{id=1, name='秦老师'}}Student{id=2, name='小红', teacher=Teacher{id=1, name='秦老师'}}Student{id=3, name='小张', teacher=Teacher{id=1, name='秦老师'}}Student{id=4, name='小李', teacher=Teacher{id=1, name='秦老师'}}Student{id=5, name='小王', teacher=Teacher{id=1, name='秦老师'}}
第二种方式【在SQL中建立连接】(按照结果嵌套查询)
添加一个接口方法(避免与上面的方法重名)
public interface StudentMapper {public List<Student> getStudent();public List<Student> getStudent2();}
配置xml文件
<select id="getStudent2" resultMap="student_teacher2">SELECT student.id as sid, student.`name` as sname,teacher.`id` as tid,teacher.`name`as tnameFROM student , teacherWHERE student.`tid`=teacher.`id`;</select><resultMap id="student_teacher2" type="student"><result property="id" column="sid"/><result property="name" column="sname"/><association property="teacher" javaType="teacher"><result property="id" column="tid"/><result property="name" column="tname"/></association></resultMap>
这个比上一个容易理解:
- 首先把SQL层面的语句直接写上(加上别名);
- 同样需要用resultMap;
- resultMap里面id与name这两个属性与sid与sname这两个字段别名(起来别名的字段就用别名替换);
- 同样是association,这里的处理类似于嵌套一个resultMap把teacher的字段与属性连接起来;
- 这样就相当于先在student表查询所有的student的所有字段,在结果中发现有一个属性字段teacher,那就再去查询teacher表。
在这种方法中,student表与teacher表的连接方式写在了SQL语句里面
结果(与上一种的结果一样):
Student{id=1, name='小明', teacher=Teacher{id=1, name='秦老师'}}Student{id=2, name='小红', teacher=Teacher{id=1, name='秦老师'}}Student{id=3, name='小张', teacher=Teacher{id=1, name='秦老师'}}Student{id=4, name='小李', teacher=Teacher{id=1, name='秦老师'}}Student{id=5, name='小王', teacher=Teacher{id=1, name='秦老师'}}
12.一对多的处理
还是上面的数据库,只是现在一个teacher对应多个student(一对多)
直接用SQL语句查询:
select t.id,t.name,s.id,s.namefrom mybatis.student as s ,mybatis.teacher as twhere t.id = s.tid;
创建实体类:
package com.xiao.entity;import org.apache.ibatis.type.Alias;@Alias("student")public class Student {private int id;private String name;private int tid;public Student(int id, String name, int tid) {this.id = id;this.name = name;this.tid = tid;}public Student() {}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getTid() {return tid;}public void setTid(int tid) {this.tid = tid;}@Overridepublic String toString() {return "\n"+"Student{" +"id=" + id +", name='" + name + '\'' +", tid=" + tid +'}'+"\n";}}
package com.xiao.entity;import org.apache.ibatis.type.Alias;import java.util.List;@Alias("teacher")public class Teacher {private int id;private String name;//每个老师有多个学生private List<Student> students;public Teacher(int id, String name, List<Student> students) {this.id = id;this.name = name;this.students = students;}public Teacher() {}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public List<Student> getStudents() {return students;}public void setStudents(List<Student> students) {this.students = students;}@Overridepublic String toString() {return "Teacher{" +"id=" + id +", name='" + name + '\'' +", students=" + students +'}';}}
第一种方式【在标签中建立连接】(与多对一一样)
TeacherMapper接口:
package com.xiao.mapper;import com.xiao.entity.Teacher;import java.util.List;public interface TeacherMapper {//public List<Teacher> getTeacher();//或取指定老师的信息与所有学生public List<Teacher> getTeacher(int id);}
StudentMapper接口(什么都没有):
package com.xiao.mapper;public interface StudentMapper {}
xml文件配置(重点)
<select id="getTeacher" resultMap="teacher_student">select * from mybatis.teacher</select><resultMap id="teacher_student" type="teacher"><result property="id" column="id"/><result property="name" column="name"/><collection property="students" ofType="student" column="id" select="getStudent"/></resultMap><select id="getStudent" resultType="student">select * from mybatis.student where tid = #{id}</select>
思路也与多对一一模一样,只是多对一是javaType,而一对多是ofType
结果:
Teacher{id=1, name='秦老师', students=[Student{id=1, name='小明', tid=1},Student{id=2, name='小红', tid=1},Student{id=3, name='小张', tid=1},Student{id=4, name='小李', tid=1},Student{id=5, name='小王', tid=1}]}
第二种方式【在SQL中建立连接】(与多对一一样)
添加一个方法(避免同名)
public List<Teacher> getTeacher2(int id);
配置xml文档
<select id="getTeacher2" resultMap="teacher_student2">select t.id as tid,t.name as tname,s.id as sid,s.name as snamefrom mybatis.student as s ,mybatis.teacher as twhere t.id = s.tid;</select><resultMap id="teacher_student2" type="teacher"><result property="id" column="tid"/><result property="name" column="tname"/><collection property="students" ofType="student"><result property="id" column="sid"/><result property="name" column="sname"/></collection></resultMap>
同样:思路与多对一一模一样,只是多对一是javaType,而一对多是ofType
12.动态SQL(随着条件的不同SQL语句发生变化)
1.动态环境搭建
CREATE TABLE `blog`(`id` VARCHAR(50) NOT NULL COMMENT '博客id',`title` VARCHAR(100) NOT NULL COMMENT '博客标题',`author` VARCHAR(30) NOT NULL COMMENT '博客作者',`create_time` DATETIME NOT NULL COMMENT '创建时间',`views` INT(30) NOT NULL COMMENT '浏览量')ENGINE=INNODB DEFAULT CHARSET=utf8
2.创建工程(pom.xml与myBatis-config.xml)
注意myBatis-config.xml里面有这一条设置,因为实体类的属性与数据库里面的字段不一样
<settings><setting name="mapUnderscoreToCamelCase" value="true"/></settings>
注意只有数据库里面是下划线命名,java实体类是驼峰命名才可以
3.工具类
package com.xiao.utils;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;import java.io.InputStream;public class MyBatisUtils {private static InputStream resource = null;private static SqlSessionFactory sqlSessionFactory = null;static {try {resource = Resources.getResourceAsStream("mybatis-config.xml");sqlSessionFactory = new SqlSessionFactoryBuilder().build(resource);} catch (IOException e) {e.printStackTrace();}}public static SqlSession getSqlSession(){return sqlSessionFactory.openSession();}}
package com.xiao.utils;import java.util.UUID;/*** Blig的Id我们选择用UUID随机获取*/public class IdGetter {public static String getId(){String id = null;id = UUID.randomUUID().toString().replaceAll("-","");return id;}}
4.实体类
package com.xiao.entity;import org.apache.ibatis.type.Alias;import java.util.Date;@Alias("blog")public class Blog {private String id;private String title;private String author;private Date createTime;private int views;public Blog(String id, String title, String author, Date create_time, int views) {this.id = id;this.title = title;this.author = author;this.createTime = create_time;this.views = views;}public Blog() {}public String getId() {return id;}public void setId(String id) {this.id = id;}public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}public String getAuthor() {return author;}public void setAuthor(String author) {this.author = author;}public Date getCreate_time() {return createTime;}public void setCreate_time(Date create_time) {this.createTime = create_time;}public int getViews() {return views;}public void setViews(int views) {this.views = views;}@Overridepublic String toString() {return "Blog{" +"id='" + id + '\'' +", title='" + title + '\'' +", author='" + author + '\'' +", create_time=" + createTime +", views=" + views +'}';}}
5.插入数据:
package com.xiao.mapper;import com.xiao.entity.Blog;public interface BlogMapper {public void insertBlog(Blog blog);}
public class myTest {@Testpublic void testInsertBlog(){SqlSession sqlSession = MyBatisUtils.getSqlSession();BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);Blog blog = new Blog();blog.setId(IdGetter.getId());blog.setTitle("java学习");blog.setAuthor("张三");blog.setCreateTime(new Date());blog.setViews(9999);mapper.insertBlog(blog);blog.setId(IdGetter.getId());blog.setTitle("python学习");blog.setAuthor("李四");mapper.insertBlog(blog);blog.setId(IdGetter.getId());blog.setTitle("c++学习");blog.setAuthor("王五");mapper.insertBlog(blog);blog.setId(IdGetter.getId());blog.setTitle("ruby学习");blog.setAuthor("王五");mapper.insertBlog(blog);blog.setId(IdGetter.getId());blog.setTitle("c++学习");blog.setAuthor("赵六");mapper.insertBlog(blog);sqlSession.commit();sqlSession.close();}}
数据如下:
| id | title | author | create_time | views | |
|---|---|---|---|---|---|
| 1 | 620ec6555f4c4dd2b8a535b1194d1f4a | java学习 | 张三 | 2021-03-21 13:18:58 | 9999 |
| 2 | 8f5890106d3d490286643a6541f4b803 | python学习 | 李四 | 2021-03-21 13:18:58 | 2000 |
| 3 | 8f5890106d3d490286643a6541f4b803 | c++学习 | 王五 | 2021-03-21 13:18:58 | 9999 |
| 4 | ba125980d3a94ea790a30e04c63055f6 | ruby学习 | 王五 | 2021-03-21 13:20:51 | 9999 |
| 5 | 46c497f3272d43498308335aa24c99bb | c++学习 | 赵六 | 2021-03-21 13:21:46 | 9999 |
动态SQL之if子句
现在有一个需求:
- 如果没有约束条件就查询所有的博客此信息,
- 如果传入title就查询这个title对应的博客,
- 如果传入author就查询该author对应的所有博客,
- 如果同时传入title与author就查询同时满足条件的博客
public interface BlogMapper {public void insertBlog(Blog blog);public List<Blog> getBlog(Map<String,String> map);}
<select id="getBlog" parameterType="map" resultType="blog">select * from mybatis.blog where true<if test="title != null">and title = #{title}</if><if test="author != null">and author = #{author}</if></select>
动态SQL的if标签十分容易理解:
- test属性是判断的条件;
- 如果条件符合,就在基础语句后面拼接上添加的语句
- 不符合就忽略
测试:
@Testpublic void testgetBlog(){SqlSession sqlSession = MyBatisUtils.getSqlSession();BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);HashMap<String, String> map = new HashMap<>();/*现在map里面什么都没有*/List<Blog> blogList1 = mapper.getBlog(map);blogList1.forEach(blog1 -> System.out.println(blog1));/*结果*/Blog{id='620ec6555f4c4dd2b8a535b1194d1f4a', title='java学习', author='张三', createTime=Sun Mar 21 13:18:58 CST 2021, views=9999}Blog{id='8f5890106d3d490286643a6541f4b803', title='python学习', author='李四', createTime=Sun Mar 21 13:18:58 CST 2021, views=2000}Blog{id='dbe467b10dde43c6b4afea2f94e0c9e9', title='c++学习', author='王五', createTime=Sun Mar 21 13:18:58 CST 2021, views=9999}Blog{id='ba125980d3a94ea790a30e04c63055f6', title='ruby学习', author='王五', createTime=Sun Mar 21 13:20:51 CST 2021, views=9999}Blog{id='46c497f3272d43498308335aa24c99bb', title='c++学习', author='赵六', createTime=Sun Mar 21 13:21:46 CST 2021, views=9999}/*现在map里面有了title这个属性*/map.put("title","c++学习");List<Blog> blogList1 = mapper.getBlog(map);blogList1.forEach(blog1 -> System.out.println(blog1));sqlSession.close();/*结果*/Blog{id='dbe467b10dde43c6b4afea2f94e0c9e9', title='c++学习', author='王五', createTime=Sun Mar 21 13:18:58 CST 2021, views=9999}Blog{id='46c497f3272d43498308335aa24c99bb', title='c++学习', author='赵六', createTime=Sun Mar 21 13:21:46 CST 2021, views=9999}/*现在map里面又有了author这个属性*/map.put("author","王五");List<Blog> blogList1 = mapper.getBlog(map);blogList1.forEach(blog1 -> System.out.println(blog1));sqlSession.close();/*结果*/Blog{id='dbe467b10dde43c6b4afea2f94e0c9e9', title='c++学习', author='王五', createTime=Sun Mar 21 13:18:58 CST 2021, views=9999}sqlSession.close();}
choose(when、otherwise)
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。
还是尝试(只是尝试)上面的问题,再加上一个判断,如果既没有title有没有author就查询“java学习”。现在用choose(when、otherwise)来解决。
<select id="getBlog2" parameterType="map" resultType="blog">select * from mybatis.blog where true<choose><when test="title != null">and title = #{title}</when><when test="author != null">and author = #{author}</when><otherwise>and title = "java学习"</otherwise></choose></select>
public void testgetBlog2(){SqlSession sqlSession = MyBatisUtils.getSqlSession();BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);HashMap<String, String> map = new HashMap<>();/*现在map里面什么都没有*/List<Blog> blogList1 = mapper.getBlog2(map);blogList1.forEach(blog1 -> System.out.println(blog1));/*结果*/Blog{id='620ec6555f4c4dd2b8a535b1194d1f4a', title='java学习', author='张三', createTime=Sun Mar 21 13:18:58 CST 2021, views=9999}/*现在map里面有了title属性*/map.put("title","c++学习");List<Blog> blogList1 = mapper.getBlog2(map);blogList1.forEach(blog1 -> System.out.println(blog1));/*结果*/Blog{id='dbe467b10dde43c6b4afea2f94e0c9e9', title='c++学习', author='王五', createTime=Sun Mar 21 13:18:58 CST 2021, views=9999}Blog{id='46c497f3272d43498308335aa24c99bb', title='c++学习', author='赵六', createTime=Sun Mar 21 13:21:46 CST 2021, views=9999}/*现在map里面有了title与author两个个属性*/map.put("author","王五");List<Blog> blogList1 = mapper.getBlog(map);blogList1.forEach(blog1 -> System.out.println(blog1));/*结果*/Blog{id='dbe467b10dde43c6b4afea2f94e0c9e9', title='c++学习', author='王五', createTime=Sun Mar 21 13:18:58 CST 2021, views=9999}Blog{id='46c497f3272d43498308335aa24c99bb', title='c++学习', author='赵六', createTime=Sun Mar 21 13:21:46 CST 2021, views=9999}sqlSession.close();}}
通过测试发现choose(when、otherwise)与if的区别:
- if可以将多个条件全部拼接起来
- choose(when,otherwise)只能拼接一个
- [如果多个when成立,只会拼接第一个,且只有当所有when‘都不成立时才会拼接otherwise]
- (所以为什么说choose(when,otherwise)像java的switch子句)
where,trim,set
where
在前面的if的例子里面,我们在select语句中加上了where true,这样保证党所有if条件的不成立时,查询所有的信息,但是,如果不想加上where true呢?
MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:
<select id="getBlog" parameterType="map" resultType="blog">select * from mybatis.blog<where><if test="title != null">title = #{title}</if><if test="author != null">and author = #{author}</if></where></select>
where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
trim(where)
如果 where 元素与期望的不太一样,也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 上面的where 元素等价的自定义 trim 元素为:
<select id="getBlog" parameterType="map" resultType="blog">select * from mybatis.blog<trim prefix="where" prefixOverrides="and |or "><if test="title != null">title = #{title}</if><if test="author != null">and author = #{author}</if></trim></select>
prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。
set
用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:
/*接口方法*/public void upDateBlog(Map<String,Object> map);
<update id="upDateBlog" parameterType="map">update mybatis.blog<set><if test="title != null">title = #{title},</if><if test="author != null">title = #{author},</if><if test="createTime != null">createTime = #{createTime},</if><if test="views != null">views = #{views}</if></set>where id = #{id}</update>
这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。
trim(set)
trim不只有where一个用法,set也可以用trim替换,与上面等价的trim是:
<update id="upDateBlog" parameterType="map">update mybatis.blog<trim prefix="set" prefixOverrides=","><if test="title != null">title = #{title},</if><if test="author != null">title = #{author},</if><if test="createTime != null">createTime = #{createTime},</if><if test="views != null">views = #{views}</if></trim>where id = #{id}</update>
sql片段
在上面的sql语句中,可以发现有一段代码重复使用了多次,sql片段的作用就是讲会多次使用的sql包装起来,每次使用时去调用
例:
<sql id="title-author"><if test="title != null">title = #{title}</if><if test="author != null">and author = #{author}</if></sql><select id="getBlog" parameterType="map" resultType="blog">select * from mybatis.blog<trim prefix="where" prefixOverrides="and |or "><include refid="title-author"/></trim></select>
在sql标签里面吧会多次用到的sql语句包装起来,用的时候使用标签引用就可以了
sql片段使用注意:
- 最好基于单表查询使用
- 片段里面不要使用where语句
forEach
现在有这样一个sql
select * from jdbcstudy.users where (id=1 or id=2 or id=3);
主要在于(id=1 or id=2 or id=3),当然,我们可以在select标签里面直接写这句sql,但如果每一次查询不同的id呢?(也就是转化为了一个动态的sql),forEach就是为了解决这个问题的。现在我们尝试完成这个工作
public List<Blog> selectBlogsForEach(List<Integer> ids);
<!-- select * from jdbcstudy.users where (id=1 or id=2 or id=3);--><select id="selectBlogsForEach" parameterType="list" resultType="blog">select * from mybatis.blog<where><foreach collection="list" item="id" open="(" close=")" separator=" or ">id = #{id}</foreach></where></select>
测试:
@Testpublic void testSelectBlogsForEach(){SqlSession sqlSession = MyBatisUtils.getSqlSession();BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);ArrayList<Integer> ids = new ArrayList<>();ids.add(1);ids.add(2);ids.add(3);List<Blog> blogs = mapper.selectBlogsForEach(ids);blogs.forEach(blog -> System.out.println(blog));sqlSession.close();}
forEach标签实际上还是sql的拼接,只是他会遍历传入的集合或列表等可迭代对象的元素,然后把每一个元素拼接进去
有五个常用标签:
- collection:传入的集合类型
- item:将集合中的元素取出来后元素的别名(方便使用)
- open/close:开头和结尾(不是必要的,但如果是where id in(…)这样的语句就不许要有“(” 与 “)”作为开头与结尾)
- separator:分隔符
另一种方法:
public List<Blog> selectBlogsForEach(Map<String,List<Integer>> map);
<select id="selectBlogsForEach" parameterType="map" resultType="blog">select * from mybatis.blog<where><foreach collection="ids" item="id" open="(" close=")" separator=" or ">id = #{id}</foreach></where></select>
这个方法中,要传入的集合是Map里面的一个元素,这是collection=”list”就应该写为collection=”ids”(ids)是集合在Map里面的属性名。
测试:
@Testpublic void testSelectBlogsForEach(){SqlSession sqlSession = MyBatisUtils.getSqlSession();BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);ArrayList<Integer> ids = new ArrayList<>();ids.add(1);ids.add(2);ids.add(3);HashMap<String, List<Integer>> map = new HashMap<String, List<Integer>>();map.put("ids",ids);List<Blog> blogs = mapper.selectBlogsForEach(map);blogs.forEach(blog -> System.out.println(blog));sqlSession.close();}
结果:
Blog{id='1', title='java学习', author='张三', createTime=Sun Mar 21 13:18:58 CST 2021, views=9999}Blog{id='2', title='python学习', author='李四', createTime=Sun Mar 21 13:18:58 CST 2021, views=2000}Blog{id='3', title='c++学习', author='王五', createTime=Sun Mar 21 13:18:58 CST 2021, views=9999}
13.缓存
MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。
一级缓存(默认开启)
默认情况下,只启用了本地的会话缓存(一级缓存),它仅仅对一个会话中的数据进行缓存(即只在sqlsession的创建与close之间开启缓存),而且,如果有
- 查询的是两个不同的数据
- 有update,delete,insert操作直接清除缓存
- 手动写入sqlSession.clearCache()操作也会清除缓存
二级缓存
- 二级缓存也叫全局缓存,由于一级缓存的作用域太低,多以诞生了二级缓存
- 二级缓存是基于namespace的缓存,一个命名空间对应一个二级缓存
工作机制:
- 一个会话查询一条数据,这条数据就会被放在当前会话的以及缓存中。
- 如果当前会话关闭了,这个会话对应的一级缓存就没了,但会话数据可以保存在二级缓存中
- 不同的mapper查处的数据会存放在自己对于的缓存中(map)中。
步骤:
1.显示开启全局缓存
<settings>......<!--其他的setting(当然,setting之间没有先后顺序)--><setting name="cacheEnabled" value="true"/></settings>
2.既然二级缓存是namespace级别的,那就需要在mapper中配置
<cache/>...<!--这是最直接的,没有说明任何参数--><!--一般直接这么写就可以-->
<cacheeviction="FIFO"flushInterval="60000"size="512"readOnly="true"/>...<!--(官方的案例)带有参数的配置--><!--这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。-->
可用的清除策略有:
LRU– 最近最少使用:移除最长时间不被使用的对象。FIFO– 先进先出:按对象进入缓存的顺序来移除它们。SOFT– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
例如:
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--与接口类绑定--><mapper namespace="com.xiao.mapper.BlogMapper"><cache eviction="FIFO"flushInterval="60000"size="512"readOnly="true"/><insert id="insertBlog" parameterType="blog">insert into mybatis.blog (id, title, author, create_time, views)values (#{id},#{title},#{author},#{createTime},#{views})</insert></mapper>
注意:
- 最好让实体类序列化
- 只要开启了二级缓存,在同一个mapper下就有效;
- 所有的数据都会先放在一级缓存中;
- 只有当会话提交或关闭,才会提交到二级缓存。
Ehcache(自定义缓存)
先导包
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache --><dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-ehcache</artifactId><version>1.1.0</version></dependency>
在mapper.xml文件中修改cache的类型
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
添加配置文件encache.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"><diskStore path="./tmpdir/Tmp_EhCache"/><defaultCacheeternal="false"maxElementsInMemory="10000"overflowToDisk="false"diskPersistent="false"timeToIdleSeconds="1800"timeToLiveSeconds="259200"memoryStoreEvictionPolicy="LRU"/><cachename="cloud_user"eternal="false"maxElementsInMemory="5000"overflowToDisk="false"diskPersistent="false"timeToIdleSeconds="1800"timeToLiveSeconds="1800"memoryStoreEvictionPolicy="LRU"/></ehcache>
