MyBatis入门 新建Maven工程,在其中引入所需要的包,如mybatis, mysql-connector-java, junit。配置pom.xml如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > xt.mybatis</groupId > <artifactId > simple</artifactId > <version > 1.0-SNAPSHOT</version > <dependencies > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.3.0</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.38</version > </dependency > </dependencies > </project >
在MySQL中新建一个mybatis的库
然后使用该数据库并建一个book表
USE mybatis; CREATE TABLE BOOK(ID INT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR (20 ), NUMBER int ); insert into BOOK(NAME,NUMBER) VALUES ('Java程序设计' ,10 ), ('数据结构' ,10 ), ('设计模式' ,10 );
如果出现由于中文不能插入的问题,输入以下语句然后插入数据
alter table mybatis change name name varchar (20 ) character set utf8;
在src/java中新建包com.xt.entity,新建Book实体类,实体类是用来保存数据库中查询到的结果,在这里实体类的属性要保持和数据库中的列名一致(后续会讲解不一致会出现什么以及解决办法)
package com.xt.entity;public class Book { private int id; private String name; private int number; 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 getNumber () { return number; } public void setNumber (int number) { this .number = number; } @Override public String toString () { return "Book{" + "id=" + id + ", name='" + name + '\'' + ", number=" + number + '}' ; } }
在src/java中新建包com.xt.dao,新建一个接口BookDao
package com.xt.dao;import com.xt.entity.Book;import java.util.List;public interface BookDao { public List<Book> findAll () ; }
在src/resources下新建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" ></transactionManager> <dataSource type="POOLED" > <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/mybatis" /> <property name="username" value="root" /> <property name="password" value="root" /> </dataSource> </environment> </environments> <mappers> <mapper resource="com/xt/dao/bookDao.xml" /> </mappers> </configuration>
mybatis-config.xml是mybatis的配置文件,我们在其中配置数据源,连接数据库的驱动,连接哪个数据库,以及用户和密码。
在src/resources下新建目录结构com/xt/dao,在dao文件夹中新建BookDao.xml
<?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 ="com.xt.mapper.BookMapper" > <select id ="findAll" resultType ="com.xt.entity.Book" > select * from book </select > </mapper >
在test/java下新建类MyBatisTest
import com.xt.mapper.BookDao;import com.xt.entity.Book;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 org.junit.Test;import java.io.*;import java.util.List;public class BookMapperTest { @Test public void testFindAll () { SqlSession sqlSession = null ; SqlSessionFactory sqlSessionFactory = null ; InputStream is = null ; try { String resource = "mybatis-config.xml" ; is = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); sqlSession = sqlSessionFactory.openSession(); BookMapper bookMapper = sqlSession.getMapper(BookMapper.class); List<Book> list = bookMapper.findAll(); System.out.println(list); } catch (IOException e) { e.printStackTrace(); } finally { try { is.close(); } catch (IOException e) { e.printStackTrace(); } if (sqlSession != null ) { sqlSession.close(); } } } }
输出为
[Book{id=1 , name='Java程序设计' , number=10 }, Book{id=2 , name='数据结构' , number=10 }, Book{id=3 , name='设计模式' , number=10 }]
如果能够顺利输出以上数据,说明配置没有问题了。
MyBatis的CRUD 保存操作 在BookDao中添加方法saveBook(Book book)
void saveBook (Book book) ;
然后在BookDao.xml中添加下面的语句
< insert id= "saveBook" parameterType= "com.xt.entity.Book"> insert into book(name, number) values (#{Book.name}, #{Book.number}); < / insert >
上面的parameterType说明该方法传入的参数类型,需要写全类名。我们在下面一行sql语句中使用了传入Book对象的属性
#{Book.name}, #{Book.number}
由于已经指定了传入的是Book类,所以Book.可以不写,即
< insert id= "saveBook" parameterType= "com.xt.entity.Book"> insert into book(name, number) values (#{name}, #{number}); < / insert >
接在在test/java下的测试类MybatisTest.java,加入下面的代码
private SqlSession sqlSession;private BookDao bookDao;@Before public void init () throws Exception { SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml" )); sqlSession = sqlSessionFactory.openSession(); bookDao = sqlSession.getMapper(BookDao.class); } @After public void destroy () { sqlSession.close(); } @Test public void testSaveBook () { Book book = new Book(); book.setName("C++" ); book.setNumber(5 ); bookDao.saveBook(book); }
对以上的程序做几点声明:
init()添加了@Before注解,表示该方法会在被@Test注解的方法执行前执行;destroy()添加了@After注解,表示在被@Test注解的方法后执行
init()中是一些初始化的代码,因为SqlSession和BookDao对象会在@Test注解的方法中用到,为了不每次都初始化,我们把SqlSession和BookDao抽离出来作为成员变量,然后在init()方法中初始化;destroy()中是释放资源的代码
在执行前我们先看数据库中的数据,以观察该保存操作是否执行成功
+----+--------------+--------+ | ID | name | NUMBER | +----+--------------+--------+ | 1 | Java程序设计 | 10 | | 2 | 数据结构 | 10 | | 3 | 设计模式 | 10 | +----+--------------+--------+
执行程序之后,数据库中的数据为
+----+--------------+--------+ | ID | name | NUMBER | +----+--------------+--------+ | 1 | Java程序设计 | 10 | | 2 | 数据结构 | 10 | | 3 | 设计模式 | 10 | +----+--------------+--------+
发现数据库中的数据并没有发现改变,难道是程序写错了。其实是因为autocommit被设置为false了,所以进行回滚操作了,我们在destroy中加入下面的语句
@After public void destroy () { sqlSession.commit(); sqlSession.close(); }
再次执行程序观察数据库中的数据
+----+--------------+--------+ | ID | name | NUMBER | +----+--------------+--------+ | 1 | Java程序设计 | 10 | | 2 | 数据结构 | 10 | | 3 | 设计模式 | 10 | | 5 | C++ | 5 | +----+--------------+--------+
我们保存对象时并没有设置id的值,因为id是自增长的。如果我们在保存数据后希望获得自增长的id值,我们可以修改BookDao.xml为
< insert id= "saveBook" parameterType= "com.xt.entity.Book"> < selectKey keyProperty= "id" keyColumn= "id" resultType= "int" order = "AFTER"> select last_insert_id(); < / selectKey> insert into book(name, number) values (#{name}, #{number}); < / insert >
接着修改测试类中的方法
@Test public void testSaveBook () { Book book = new Book(); book.setName("C++" ); book.setNumber(5 ); System.out.println("更新前的Book对象" ); System.out.println(book); bookDao.saveBook(book); System.out.println("更新后的Book对象" ); System.out.println(book); }
运行结果为
更新前的Book对象 Book{id=null , name='C++' , number=5 } 更新后的Book对象 Book{id=6 , name='C++' , number=5 }
可见已经获得了自增长的id。
更新操作 在BookDao中添加updateBook(Book book)方法
void updateBook (Book book) ;
然后在BookDao.xml中添加如下
< update id= "updateBook" parameterType= "com.xt.entity.Book"> update book set name = #{name}, number = #{number} where id = #{id}; < / update>
现在在测试类中添加测试方法
@Test public void testUpdateBook () { Book book = new Book(); book.setId(5 ); book.setName("PHP" ); book.setNumber(20 ); bookDao.updateBook(book); }
执行并观察数据库中的数据
+----+--------------+--------+ | ID | name | NUMBER | +----+--------------+--------+ | 1 | Java程序设计 | 10 | | 2 | 数据结构 | 10 | | 3 | 设计模式 | 10 | | 5 | PHP | 20 | +----+--------------+--------+
删除操作 在BookDao中添加deleteBook(Integer id)方法
void deleteBook (Integer id) ;
在BookDao.xml中添加如下
< delete id= "deleteBook" parameterType= "int"> delete from book where id = #{id}; < / delete >
这里的parameterType写为了int,其实这里可以写为INT,integer,Integer均可,在sql语句中使用了#{id},其实这里可以随便写,如#{uid},#{bookId},因为只传入一个值,不管写什么都会被认为是这个传入的值。现在在测试类中添加测试方法
@Test public void testDeleteBook () { bookDao.deleteBook(5 ); }
执行并且查看数据库中的数据如下
+----+--------------+--------+ | ID | name | NUMBER | +----+--------------+--------+ | 1 | Java程序设计 | 10 | | 2 | 数据结构 | 10 | | 3 | 设计模式 | 10 | +----+--------------+--------+
查询操作 查询一个 在BookDao中添加findById(Integer id)方法
Book findById (Integer id) ;
在BookDao.xml中添加如下
< select id= "findById" parameterType= "int" resultType= "com.xt.entity.Book"> select * from book where id = #{id}; < / select >
上面的resultType声明了返回的数据类型,接下来在测试了中添加测试方法
@Test public void testFindById () { Book book = bookDao.findById(1 ); System.out.println(book); }
输出结果为
Book{id=1 , name='Java程序设计' , number=10 }
模糊查询 在BookDao中添加findByName(String name)方法
List<Book> findByName (String name) ;
在BookDao.xml中添加如下
< select id= "findByName" parameterType= "string" resultType= "com.xt.entity.Book"> select * from book where name like #{name} < / select >
接下来在测试了中添加测试方法
@Test public void testFindByName () { List<Book> list = bookDao.findByName("%设计%" ); for (Book book:list) { System.out.println(book); } }
输出为
Book{id=1 , name='Java程序设计' , number=10 } Book{id=3 , name='设计模式' , number=10 }
当初我在使用中文模糊查询时查询不出来,使用英文可以,去网上查了一下,解决办法是是在my.ini文件的最后加上
character-set-server = utf8 collation-server = utf8_general_ci
然后重启mysql服务器。具体可以参考这个链接中文模糊查询 。
查询一个值 比如我想查询数据库中有多少条数据,在BookDao中添加findTotal()方法
在BookDao.xml中添加如下
< select id= "findTotal" resultType= "int"> select count (id) from book; < / select >
接下来在测试了中添加测试方法
@Test public void testFindTotal () { int numbers = bookDao.findTotal(); System.out.println(numbers); }
输出为
resultMap 上面我们要求实体类Book的属性要与数据库中类的列名相同,如果列名不同呢? 我们修改Book的实体类
package com.xt.entity;import java.io.Serializable;public class Book implements Serializable { private Integer bookId; private String bookName; private int bookNumber; }
现在修改测试类中的Setxxx方法,以及BookDao.xml中的#{Xxx},如修改#{name}为#{bookName},接着运行findAll的测试方法,得到
可见数据没有封装。这个时候有两种解决办法
先介绍起别名,我们修改findAll的sql语句为
< select id= "findAll" resultType= "com.xt.entity.Book"> select id bookId, name bookName, number bookNumber from book < / select >
接着运行findAll的测试方法,结果为
Book{bookId=1 , bookName='Java程序设计' , bookNumber=10 } Book{bookId=2 , bookName='数据结构' , bookNumber=10 } Book{bookId=3 , bookName='设计模式' , bookNumber=10 } Book{bookId=6 , bookName='C++' , bookNumber=5 }
可见数据封装好了。这种方法执行效率很高,但是所有的select语句都需要起别名,这里介绍resultMap,只要将实体类的属性名与数据库列名对应好,下面只需要引用即可,只需要写一次,在
<mapper namespace ="com.xt.dao.BookDao" > <resultMap id ="bookMap" type ="com.xt.entity.Book" > <id property ="bookId" column ="id" > </id > <result property ="bookName" column ="name" > </result > <result property ="bookNumber" column ="number" > </result > </resultMap > <select id ="findAll" resultMap ="bookMap" > select * from book </select > <select id ="findById" parameterType ="int" resultMap ="bookMap" > select * from book where id = #{id}; </select > <select id ="findByName" parameterType ="string" resultMap ="bookMap" > select * from book where name like #{bookName} </select > ... ... </mapper >
再次运行findAll的测试方法,输出为
Book{bookId=1 , bookName='Java程序设计' , bookNumber=10 } Book{bookId=2 , bookName='数据结构' , bookNumber=10 } Book{bookId=3 , bookName='设计模式' , bookNumber=10 } Book{bookId=6 , bookName='C++' , bookNumber=5 }
动态SQL 所谓的动态SQL指的是SQL语句不是不变的,比如根据是否传入age以决定是否加入age验证项,下面介绍三种动态SQL。
加入有下面User表
+ | user_name | age | + | 张三 | 18 | | 李四 | 19 | +
实体类以及UserDao接口就不演示了。
if 首先在UserDao中创建下面的方法
List<User> findByCondition (User user) ;
该方法根据传入的User对象去查询,主要是根据该User对象所包含的userName和age去查询,但是这个User对象不一定设置了这两个属性,如
User user = new User(); user.setAge(18 ); userDao.findByCondition(user);
上面的Java代码只设置了age的值,这意味着我们在userDao.xml中的select语句不能写成这样
select user_name userName, age from user where user_name = #{userName} and age = #{age}
假设如果userName没有设置的话,那么where后面的结果永远是false,意味着查不到结果,而我们希望的是,如果存在什么,则根据什么是查,比如如果不存在userName,存在age,则相应的SQL语句应该是
select user_name userName, age from user where age = #{age}
如果存在userName而不存在age,则只根据userName查,如果都不存在,则得到所有的User,即
select user_name userName, age from user
这个时候仅仅靠我们上面的手段是得不到的,因为这时的SQL语句是动态的。明显,我们需要对userName和age进行判断,以决定是否加入到SQL语句中,而判断使用的就是if。满足上面要求的SQL语句如下
< select id= "findByCondition" parameterType= "com.xt.domain.User" resultType= "com.xt.domain.User"> select user_name userName, age from user where 1 = 1 < if test= "userName != null"> and user_name = #{userName} < / if> < if test= "age != null"> and age = #{age} < / if> < / select >
上面的if标签就是对User对象中的属性进行判断,判断语句要写在if标签的test属性中,如果为真,则if标签包含的内容则会添加到SQL语句后,否则不会添加。
where 大家注意上面的SQL语句中有
那么为什么要加上1 = 1呢? 假设如果不加1 = 1,并且传过来的User对象的userName和age都为null,那么最后的SQL语句是什么样子?
select user_name userName, age from user where
这明显是一个错误的SQL语句,但是如果加上1 = 1,即使userName和age都为null,SQL也是正确的SQL语句
select user_name userName, age from user where 1 = 1
但是这样未免有点hack的意味,像是一种奇淫技巧,那mybatis有没有提供比较好的写法,答案就是where标签了,现在我们将上面的sql语句改为
select user_name userName, age from user < where > < if test= "userName != null"> and user_name = #{userName} < / if> < if test= "age != null"> and age = #{age} < / if> < / where >
我们将所有的if标签都放到了where标签里面,如果所有的if都不满足的话,那么在SQL语句中就不会出现where,如果if至少有一个成立的话,则会去掉第一个成立if标签中包含的and。假设该User对象包含userName不包含age,则最后的SQL语句是
select user_name userName, age from user where user_name = #{userName}
forEach 现在有一种的新的情况,如果传过来的参数是一个集合或者数组,那怎么处理呢? 在UserDao接口中添加下面的方法
List<User> findByAges (List<Integer> ages) ;
该方法会接收一个List集合,我们的要求是去查询用户年龄在ages集合中的用户,相应的SQL语句如下
< select id= "findByAges" resultType= "com.xt.domain.User" parameterType= "java.util.List"> select user_name userName, age from user < where > < if test= "list != null"> < foreach collection= "list" item= "age" open = "and age in(" close = ")" separator= ","> #{age} < / foreach> < / if> < / where > < / select >
上面的where和if标签想必不用再解释了,重点是forEach标签。forEach的作用就是遍历集合创建一个SQL语句。forEach中的collection标签的值就是传过来的list集合,item就是集合中的元素,forEach会遍历该集合拿到这些item,open是指创建的SQL语句以什么开头,close是以什么结尾,而separator是指分隔符是什么,而forEach中包含的内容即是分隔符分隔的一个个元素。所以假设传来这么一个List
List<Integer> list = new ArrayList<Integer>(); list.append(15 ); list.append(18 );
那么上面的SQL语句最后会被生成为
select user_name userName, age from user where age in (15 , 18 )
sql片段 在上面,我们发现上面的SQL语句都包含这么一条SQL语句
select user_name userName, age from user
既然这是重复的内容,那我们就可以将其抽离出来成一个SQL片段,然后在SQL语句中引用该片段,在userDao.xml中的mapper标签下,我们加入以下内容
< sql id= "default"> select user_name userName, age from user < / sql >
上面的id名可以任意的取,这里的id是为了在后面引用该SQL片段。接下来将所有select中的
select user_name userName, age from user
替换为
< include refid= "default"> < / include>
如
< select id= "findByAges" resultType= "com.xt.domain.User" parameterType= "java.util.List"> < include refid= "selectAll"> < / include> < where > < if test= "list != null"> < foreach collection= "list" item= "age" open = "and age in(" close = ")" separator= ","> #{age} < / foreach> < / if> < / where > < / select >
多表查询 一对多与多对一 假设有两张表,用户表和账户表,二者之间的关系为
现在我们的需求的是当我们查询用户时,同时查询它所包含的所有账户,当我们查询账户时,同时查询它所对应的用户。
现在创建两张表user和account。
DROP TABLE IF EXISTS `user `;CREATE TABLE `user ` ( `id` int (11 ) NOT NULL auto_increment, `username` varchar (32 ) NOT NULL COMMENT '用户名称' , `birthday` datetime default NULL COMMENT '生日' , `sex` char (1 ) default NULL COMMENT '性别' , `address` varchar (256 ) default NULL COMMENT '地址' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8; insert into `user `(`id`,`username`,`birthday`,`sex`,`address`) values (41 ,'老王' ,'2018-02-27 17:47:08' ,'男' ,'北京' ), (42 ,'小王' ,'2018-03-02 15:09:37' ,'女' ,'北京金燕龙' ), (45 ,'张三' ,'2018-03-07 17:37:26' ,'男' ,'北京' ),(46 ,'超级玛丽' ,'2018-03-08 11:44:00' ,'女' ,'北京修正' );DROP TABLE IF EXISTS `account`;CREATE TABLE `account` ( `ID` int (11 ) NOT NULL COMMENT '编号' , `UID` int (11 ) default NULL COMMENT '用户编号' , `MONEY` double default NULL COMMENT '金额' , PRIMARY KEY (`ID`), KEY `FK_Reference_8` (`UID`), CONSTRAINT `FK_Reference_8` FOREIGN KEY (`UID`) REFERENCES `user ` (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8; insert into `account`(`ID`,`UID`,`MONEY`) values (1 ,46 ,1000 ),(2 ,45 ,1000 ),(3 ,46 ,2000 );
现在这两张表的内容为
# user + | id | username | birthday | sex | address | + | 41 | 老王 | 2018 -02 -27 17 :47 :08 | 男 | 北京 | | 42 | 小王 | 2018 -03 -02 15 :09 :37 | 女 | 北京金燕龙 | | 45 | 张三 | 2018 -03 -07 17 :37 :26 | 男 | 北京 | | 46 | 超级玛丽 | 2018 -03 -08 11 :44 :00 | 女 | 北京修正 | + # account + | ID | UID | MONEY | + | 1 | 46 | 1000 | | 2 | 45 | 1000 | | 3 | 46 | 2000 | +
现在创建对应的实体类以及对应的接口以及相应的xml文件。二者的实体类如下
package entity;import java.util.Date;import java.util.List;public class User { private Integer id; private String name; private String sex; private Date birthday; private String address; private List<Account> accounts; public List<Account> getAccounts () { return accounts; } public void setAccounts (List<Account> accounts) { this .accounts = accounts; } public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public String getName () { return name; } public void setName (String name) { this .name = name; } public String getSex () { return sex; } public void setSex (String sex) { this .sex = sex; } public Date getBirthday () { return birthday; } public void setBirthday (Date birthday) { this .birthday = birthday; } public String getAddress () { return address; } public void setAddress (String address) { this .address = address; } @Override public String toString () { return "User{" + "id=" + id + ", name='" + name + '\'' + ", sex='" + sex + '\'' + ", birthday=" + birthday + ", address='" + address + '\'' + ", accounts=" + accounts + '}' ; } }
package entity;public class Account { private Integer id; private Integer uid; private Double money; private User user; public User getUser () { return user; } public void setUser (User user) { this .user = user; } public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public Integer getUid () { return uid; } public void setUid (Integer uid) { this .uid = uid; } public Double getMoney () { return money; } public void setMoney (Double money) { this .money = money; } @Override public String toString () { return "Account{" + "id=" + id + ", uid=" + uid + ", money=" + money + ", user=" + user + '}' ; } }
仔细注意这两个实体类,发现在User实体类中有一个Account的List列表,因为一个用户可以有多个账户。而在Account中有一个User对象,因为一个账户对应一个用户。
现在我们来实现第一个需求,当查询所有用户时,同时显示每个用户下的所有账户信息,在UserDao中加入下面的方法
List<User> findAllUsers () ;
下面在userDao.xml中加入下面的查询语句
< mapper namespace= "dao.UserDao"> < resultMap id= "users" type= "entity.User"> < id column = "id" property= "id"> < / id> < result column = "username" property= "userName"> < / result > < result column = "sex" property= "sex"> < / result > < result column = "birthday" property= "birthday"> < / result > < result column = "address" property= "address"> < / result > < collection property= "accounts" ofType= "entity.Account"> < id column = "aid" property= "id"> < / id> < result column = "uid" property= "uid"> < / result > < result column = "money" property= "money"> < / result > < / collection> < / resultMap> < select id= "findAllUsers" resultMap= "users"> select user.* , account.id aid, account.uid, account.money from user , account where user.id = account.uid < / select > < / mapper>
由于User中包含一个Account类型的List,所以要设置这个List的对应规则,正是上面collection标签的部分,property是User中List对象的名称,ofType是List集合的类型。collection包含的部分就是List元素对应的规则。
现在实现第二个需求,当查询账户时,同时查询出对应的User信息,在AccountDao中添加下面的方法
List<Account> findAllAccounts () ;
在accountDao.xml添加内容如下
< mapper namespace= "dao.AccountDao"> < resultMap id= "accounts" type= "entity.Account"> < id property= "id" column = "aid"> < / id> < result property= "uid" column = "uid"> < / result > < result property= "money" column = "money"> < / result > < association property= "user" javaType= "entity.User"> < id property= "id" column = "id"> < / id> < result property= "userName" column = "username"> < / result > < result property= "sex" column = "sex"> < / result > < result property= "birthday" column = "birthday"> < / result > < result property= "address" column = "address"> < / result > < / association> < / resultMap> < select id= "findAllAccounts" resultMap= "accounts"> select user.* , account.id aid, account.uid, account.money from account left join user on user.id = account.uid < / select > < / mapper>
在Account中有一个User对象,我们要设置这个的对应规则,使用的就是association,property就是Account中User对象的名称,javaType是指对应的实体类。
多对多 其实多对多查询与一对多的查询是一样的,上述我们在User类中添加了一个类型为Account的List集合,已表示一对多,在Account中有一个User对象,已表示一对一。
那要如果表示多对多,假设一个账户对应多个用户,那么只要在Account中将一个User对象改为一个类型为User的List列表。
延迟加载 延迟加载又称为懒加载,又称按需加载,指的就是在需要的时候才加载,比如上面查询所有的User,但是这时不需要立即加载对应的账户信息,而是等我们使用账户信息时在进行加载,比如
这时再向数据库发起查询。
MyBatis根据对关联对象查询的select语句的执行时机,分为三种类型:直接加载、侵入式加载与深度延迟加载
直接加载:执行完对主加载对象的select语句,马上执行对关联对象的select查询。
侵入式延迟:执行对主加载对象的查询时,不会执行对关联对象的查询。但当要访问主加载对象的某个属性(该属性不是关联对象的属性)时,就会马上执行关联对象的select查询。
深度延迟:执行对主加载对象的查询时,不会执行对关联对象的查询。访问主加载对象的详情时也不会执行关联对象的select查询。只有当真正访问关联对象的详情时,才会执行对关联对象的select查询。
Mybatis的延迟加载,需要通过resultMap标签中的association和collection子标签才能演示成功。
要设置延迟加载,需要在mybatis-config.xml中设置,如下
<configuration > <settings > <setting name ="lazyLoadingEnabled" value ="true" /> <setting name ="aggressiveLazyLoading" value ="false" /> </settings > ... ... </configuration >
lazyLoadingEnabled是设置是否深度延迟,当设置为true,就是设置为深度延迟,即当访问关联对象的详细信息时才会进行查询。aggressiveLazyLoading是用来设置侵入式延迟,默认为false,这里本应不用设置,只是为了演示。
一对一延时加载 这里演示当查询账户时,同时查询对应的用户信息。首先我们看原先的查询语句
select user.* , account.id aid, account.uid, account.money from account left join user on user.id = account.uid
如果这么写的话,会立即查询相应User的信息,所以必须更改上面的sql语句
同时修改resultMap如下
< resultMap id= "accounts" type= "entity.Account"> < id property= "id" column = "id"> < / id> < result property= "uid" column = "uid"> < / result > < result property= "money" column = "money"> < / result > < association property= "user" javaType= "entity.User" select = "dao.UserDao.findById" column = "uid"> < / association> < / resultMap>
注意到我们将association中的对应规则删除了,因为在上面的sql语句中我们只查询了account的内容。但是我们增加了两个属性,一个是select属性,这个是当我们使用account对象访问其中的User属性时会调用的sql语句,比如
这时因为会访问Account关联对象User的具体信息,会调用userDao的findbyId(Integer id)进行查询,从而得到相应的User信息。
第二个参数是column,其值是上面findById方法所需要的id。
这里向UserDao中增加下面的方法
User findById (Integer id) ;
并且在userDao.xml中增加select标签
< select id= "findById" resultType= "entity.User" parameterType= "Integer"> select * from user where id = #{id} < / select >
此刻万事大吉,这里贴出accountDao.xml中的内容
< resultMap id= "accounts" type= "entity.Account"> < id property= "id" column = "id"> < / id> < result property= "uid" column = "uid"> < / result > < result property= "money" column = "money"> < / result > < association property= "user" javaType= "entity.User" select = "dao.UserDao.findById" column = "uid"> < / association> < / resultMap> < select id= "findAllAccounts" resultMap= "accounts"> select * from account < / select >
一对多延时加载 一对多延时加载与一对一延时加载是一样,只不过association换成了collection。
这里我们以查询User信息时,同时查询该用户包含的所有账户,这里修改findAllUsers对应的sql语句为
< select id= "findAllUsers" resultMap= "users"> select * from user < / select >
修改对应的resultMap为
< resultMap id= "users" type= "entity.User"> < id column = "id" property= "id"> < / id> < result column = "username" property= "userName"> < / result > < result column = "sex" property= "sex"> < / result > < result column = "birthday" property= "birthday"> < / result > < result column = "address" property= "address"> < / result > < collection column = "id" property= "accounts" ofType= "entity.Account" select = "dao.AccountDao.findByUid"> < / collection> < / resultMap>
这时上面的代码已经不需要解释了,在AccountDao中添加findByUid的方法
List<Account> findByUid (Integer uid) ;
同时在accountDao.xml中添加对应的select标签
< select id= "findByUid" resultType= "entity.Account" parameterType= "Integer"> select * from account where uid = #{uid} < / select >
缓存 所谓缓存,指的就是当我们向数据库查询数据时,将数据保存在内存中,当第二次查询时,不再向数据库进行查询,而是直接从内存拿数据,这就是缓存。由于数据在内存中,从内存中拿数据比从数据库中拿数据快很多,并且也可以减少数据库的压力。
一级缓存 MyBatis中的一级缓存的范围是sqlSession,即相当于在sqlSession中有一个localCache,当sqlSession查询数据时会将数据保存在这个cache中,当该sqlSession再次查询数据时,会先在这个cache中查询数据,如果有,则会直接拿内存中的数据,如果没有,则会去数据库查询。
那怎么保证拿到的数据不会错呢? 比如当更新数据后,如果继续从缓存中拿数据,这时拿到的数据就会是错的,所以当发生更新、删除、commit()等操作时,就会将一级缓存清空,这就意味着当更新数据后,就得直接去数据库中拿数据了。当SqlSession对象调用clearCache(),也会清除sqlSession的缓存。
那么怎么开启一级缓存呢? 在mybatis-config.xml中添加下面的setting
<setting name ="localCacheScope" value ="SESSION" />
localCacheScope有两种取值,一种是SESSION,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存;一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。默认是SESSION级别,这就意味着一级缓存默认是开启的,上面的设置不用写。
二级缓存 想象有两个sqlSession,暂且称之为sqlSession1和sqlSession2,二者连接了同一个表,假设sqlSession1对表进行了更新,sqlSession2向数据库查询数据,但是由于一级缓存的存在,并且sqlSession2没有进行增加、删除等等操作,意味着sqlSession2没有清楚缓存,所以sqlSession2拿到的是缓存中的数据,这种数据称之为脏数据。
那怎么解决这个问题? 这个时候就要使用二级缓存。二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
那怎么开启二级缓存呢? 首先在mybatis-config.xml加入下面的设置
<setting name ="cacheEnabled" value ="true" />
接着在xxxDao.xml中的namespace(mapper标签内中)加入
如果希望局部关闭二级缓存时,比如某查询标签关闭二级缓存,则在标签的属性useCache设置为false,如
< select id= "findAllUsers" resultMap= "users" useCache= "false"> select * from user < / select >
二级缓存对于不同的命名空间namespace的数据是互不干扰的,倘若多个namespace中对一个表进行操作的话,就会导致这不同的namespace中的数据不一致的情况。例如,在单表上使用二级缓存 在做关联关系查询时,就会发生多表的操作,此时有可能这些表存在于多个namespace中,这就会出现上面出现的问题了。
总结:
MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
使用注解开发 使用注解开发,不需要对应的xxxDao.xml文件,因为我们将sql语句写在XxxDao接口的注解中。
简单使用 新建一个工程,配置好pom.xml文件以及mybatis-config.xml的主配置文件,在entity包下新建User实体类,在dao包下新建UserDao接口,内容如下
package dao;import entity.User;import org.apache.ibatis.annotations.Select;import java.util.List;public interface UserDao { @Select("select * from user") List<User> findAll () ; }
在test/java/TestUser类中进行单元测试
import dao.UserDao;import entity.User;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 org.junit.After;import org.junit.Before;import org.junit.Test;import java.io.InputStream;import java.util.List;public class TestUser { private UserDao userDao; private SqlSession sqlSession; private InputStream inputStream; @Before public void init () throws Exception { String resource = "mybatis-config.xml" ; inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); sqlSession = sqlSessionFactory.openSession(); userDao = sqlSession.getMapper(UserDao.class); } @Test public void testFindAll () { List<User> users = userDao.findAll(); System.out.println(users); } @After public void destroy () throws Exception { if (sqlSession != null ) { sqlSession.close(); } if (inputStream != null ) { inputStream.close(); } } }
输出为
[User{id=41 , userName='老王' , sex='男' , birthday=Tue Feb 27 17 :47 :08 CST 2018 , address='北京' }, User{id=42 , userName='小王' , sex='女' , birthday=Fri Mar 02 15 :09:37 CST 2018 , address='北京金燕龙' }, User{id=45 , userName='张三' , sex='男' , birthday=Wed Mar 07 17 :37 :26 CST 2018 , address='北京' }, User{id=46 , userName='超级玛丽' , sex='女' , birthday=Thu Mar 08 11 :44 :00 CST 2018 , address='北京修正' }]
其他CRUD 这里简单的演示实验其他的CRUD操作。
更新操作 在UserDao中添加如下方法
@Update("update user set username = #{userName} where id = #{id}") void updateUser (User user) ;
在测试类中进行测试
@Test public void testUpdateUser () { User user = new User(); user.setId(45 ); user.setUserName("李四" ); userDao.updateUser(user); System.out.println(userDao.findAll()); }
输出为
[User{id=41 , userName='老王' , sex='男' , birthday=Tue Feb 27 17 :47 :08 CST 2018 , address='北京' }, User{id=42 , userName='小王' , sex='女' , birthday=Fri Mar 02 15 :09:37 CST 2018 , address='北京金燕龙' }, User{id=45 , userName='李四' , sex='男' , birthday=Wed Mar 07 17 :37 :26 CST 2018 , address='北京' }, User{id=46 , userName='超级玛丽' , sex='女' , birthday=Thu Mar 08 11 :44 :00 CST 2018 , address='北京修正' }]
如果发现不能插入中文,则修改mybatis-config.xml中的dataSource中的url为
<property name ="url" value ="jdbc:mysql://localhost:3306/mybatis?useUnicode=true& characterEncoding=utf8" />
插入操作 在UserDao中添加下面的方法
@Insert("insert into user(username, sex, address, birthday) values(#{userName}, #{sex}, #{address}, #{birthday})") void insertUser (User user) ;
在测试类中进行测试
@Test public void testInsertUser () { User user = new User(); user.setUserName("奥特曼" ); user.setSex("男" ); user.setAddress("M78" ); user.setBirthday(new Date()); userDao.insertUser(user); System.out.println(userDao.findAll()); }
删除操作 在UserDao中添加下面的方法
@Delete("delete from user where id = #{id}") void deleteUserById (Integer id) ;
在测试类中进行测试
@Test public void testDeleteUserById () { userDao.deleteUserById(48 ); for (User user: userDao.findAll()) { System.out.println(user); } }
在注解中配置resultMap 如下
@Select("select * from user") @Results(value = { @Result(id=true, column = "id", property = "id"), @Result(column = "username", property = "userName"), @Result(column = "sex", property = "sex"), @Result(column = "birthday", property = "birthday"), @Result(column = "address", property = "address") }) List<User> findAll () ;
如果直接将Results写在方法上,那就意味则如果还有方法需要些resultMap,那么就还需要在写一遍,我们希望就是只写一遍resultMap,然后通过id引用即可,我们给上面的Results添加id
@Results(id = "users", value = { @Result(id=true, column = "id", property = "id"), @Result(column = "username", property = "userName"), @Result(column = "sex", property = "sex"), @Result(column = "birthday", property = "birthday"), @Result(column = "address", property = "address") })
然后在需要resultMap的方法上添加如下的注解
@Results(value = {"users"})
或者简写为
多表查询 一对一 @Select("select * from account") @Results(id = "accounts", value = { @Result(id = true, column = "id", property = "id"), @Result(column = "uid", property = "uid"), @Result(column = "money", property = "money"), @Result(column = "uid", property = "entity.User", one = @One(select = "dao.UserDao.findById", fetchType = FetchType.EAGER)), }) List<Account> findAll () ;
Result中的one就是用来设置一对一查询的,其值是一个One注解,其中的select不用解释,与在xml中association设置的select相同功能
fetchType是用来设置是立即加载还是延迟加载的
FetchType.EAGER:立即加载
FetchType.LAZY:延迟加载
注意,如果要使用延迟加载,得先在mybatis-config.xml中开启延迟加载(方法同xml配置方式)。
一对多 @Select("select * from user") @Results(value = { @Result(id=true, column = "id", property = "id"), @Result(column = "username", property = "userName"), @Result(column = "sex", property = "sex"), @Result(column = "birthday", property = "birthday"), @Result(column = "address", property = "address"), @Result(column = "id", property = "accounts", many = @Many(select = "dao.AccountDao.findByUid", fetchType = FetchType.LAZY)) }) List<User> findAll () ;
将one换成了many,对应的Many注解里面的内容不用多加解释。
缓存 如何在注解中使用缓存,对于一级缓存来说,与xml相同,不用设置,默认是SESSION。
对于二级缓存,首先在mybatis-config.xml开启缓存,与在xml中相同。
<setting name ="cacheEnabled" value ="true" > </setting >
在xml中,我们是在namespace中设置
但是在注解中,我们在UserDao接口上添加@CacheNameSpace注解,设置其blocking属性为true,如下
@CacheNamespace(blocking = true) public interface UserDao { ... }
这样就开启二级缓存了。