MyBatis框架学习-3
MyBatis框架安排
目录
3. 第三天(涉及到的应用都会要用):
1. mybatis连接池以及事务控制
-
连接池:连接池就是一个存储连接对象的容器,mybatis可以从这个容器从获取连接数据库的连接对象,并且这个容器是线程安全的,是为了避免同时从一个容器中获取到相同的连接对象,因此该容器还必须实现队列的特性,先进先出(用的先从头部取,用完从尾部添加,形成循环队列)
-
mybatis连接池以及分析
mybatis提供了三种连接池配置方式
配置位置:
配置主配置文件SqlMapConfig.xml的dataSource标签下的type属性,有以下三种取值:POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis有针对此规范的实现 UNPOOLED:采用传统的连接方式,虽然也实现了javax.sql.DataSource规范,但没有池的概念 JNDI:采用服务器提供的JDNI技术实现来获取DataSource对象,不同的服务器能拿到不同的对象 注意:如果不是web或maven的war工程,是不能使用的,我们平时所使用的tomcat服务器,采用的连接池是dbcp连接池。
可以再idea开发工具中Gtrl+N搜索PoolDataSource和UNPoolDataSource查看源码
<mark>PoolDataSource与UnpooledDataSource:源码的简单分析</mark>
POOLED:是在连接池中获取连接对象的,连接池又分为空闲连接池(idleConnections)和活动连接池(activeConnections)两部分,如果空闲连接已经没了(分配完了),则取活动连接池里面取出来,当活动连接池都满了(即每个连接都在使用当中)则会取出最早(也叫oldestActiveConnection)进入活动连接池的那个连接对象加以改造,然后取出使用,当用完之后,将连接对象返回连接池中。
UNPOOLED:比较简单,则是直接从配置文件加载配置信息,然后创建连接Connection,没有连接池,所以用完之后直接关闭销毁了。
-
mybatis事务控制的分析
- 回顾事务常见的面试题
- 什么是事务
- 事务的四大特性ACID
- 不考虑隔离性会产生的3个问题
- 四种隔离界级别
答案:- _ -
- mybatis是可以支持事务的自动提交的,如下
- 回顾事务常见的面试题
2. mybatis基于xml的动态sql语句使用
-
sql注入:是为了防止黑客等不法分子恶意注入表单,拼接sql语句去匹配数据库,导致被数据库正常解析而引起的后台信息泄露问题。
-
动态sql:在jdbc中使用preparedStatement对象处理,又称SQL的预编译处理,请看:
-
按照条件去查询
<!--根据条件查询查询--> <select id="findByCondition" resultType="uSER" resultMap="userMap"> <!-- 取别名来和实体类产生关系 --> SELECT * from USER where 1=1 <if test="userName!=null"> and username = #{userName}</if> </select>
其中 1 = 1可以直接使用where标签替换,如
<!--根据条件查询查询--> <select id="findByCondition" resultType="uSER" resultMap="userMap"> <!-- 取别名来和实体类产生关系 --> SELECT * from USER <where> <if test="userName!=null"> and username = #{userName} </if> </where> </select>
还可以给出多个查询条件
<!--根据条件查询查询--> <select id="findByCondition" resultType="uSER" resultMap="userMap"> <!-- 取别名来和实体类产生关系 --> SELECT * from USER <where> <if test="userName!=null"> and username = #{userName} </if> <if test="userSex!=null"> and sex = #{userSex} </if> </where> </select>
测试:
@org.junit.Test public void testFindCondition() throws IOException { User user = new User(); user.setUserName("传智播客"); user.setUserSex("男"); List<User> users = userDao.findByCondition(user); for (User u: users) { System.out.println(u); } }
3. mybatis的多表操作
-
表与表之间的关系
- 多对一:用户和订单就是一对多订单和用户就是多对一
- 一对多:一个用户可以下多个订单多个订单属于同一个用户
- 一对一:人和身份证号就是一对一,一个人只能有一个身份证号一个身份证号
- 多对多:只能属于一个人老师和学生之间就是多对多一个学生可以被多个老师教过一个老师可以交多个学生
- 特例:
如果拿出每一个订单,他都只能属于一个用户。所以Mybatis就把多对一看成了一对一。I
-
示例1:用户和账户
一个用户可以有多个账户
一个账户只能属于一个用户(多个账户也可以属于同一个用户)
步骤:- 建立两张表:用户表,账户表
让用户表和账户表之间具备一对多的关系:需要使用外键在账户表中添加 - 建立两个实体类:用户实体类和账户实体类让用户和账户的实体类能体现出来一对多的关系
- 建立两个配置文件用户的配置文件账户的配置文件
- 实现配置:
当我们查询用户时,可以同时得到用户下所包含的账户信息当我们查询账户时,可以同时得到账户的所属用户信息
- 建立两张表:用户表,账户表
<mark>案例需求1</mark>:一对一查询(多对一)
本次案例主要以最为简单的用户和账户的模型来分析Mybatis多表关系。用户为User 表,账户为Account 表。一个用户(User)可以有多个账户(Account)。具体关系如下:
查询所有账户信息,关联查询下单用户信息。
注意:
因为一个账户信息只能供某个用户使用,所以从查询账户信息出发关联查询用户信息为一对一查询。
如果从用户信息出发查询用户下的账户信息则为一对多查询,因为一个用户可以有多个账户。
方案一:
-
准备好Account类
public class Account implements Serializable{ 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; } @Override public String toString() { return "Account{" + "id=" + id + ", uid=" + uid + ", money=" + money + ", 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; } }
-
编写SQL语句
SELECT account.*, user.username, user.address FROM account, user WHERE account.uid = user.id
-
创建AccountUser继承Account
package com.liuzeyu.domin; public class AccountUser extends Account{ private String username; private String address; @Override public String toString() { return super.toString() +" AccountUser{" + "username='" + username + '\'' + ", address='" + address + '\'' + '}'; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }
-
定义账户的持久层 Dao 接口
public interface IAccountDao { /** * 查询所有账户,同时获取账户的所属用户名称以及它的地址信息 */ List<AccountUser> findAll(); }
-
定义 AccountDao.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.liuzeyu.dao.IAccountDao"> <!-- 配置查询所有操作--> <select id="findAll" resultType="accountuser"> select a.*,u.username,u.address from account a,user u where a.uid =u.id; </select> </mapper
注意:因为上面查询的结果中包含了账户信息同时还包含了用户信息,所以我们的返回值类型 returnType 的值设置为 AccountUser 类型,这样就可以接收账户信息和用户信息了
-
创建 AccountTest 测试类
/* <p>Title: MybastisCRUDTest</p> * <p>Description: 一对多账户的操作</p> * */ public class AccountTest { private InputStream in ; private SqlSessionFactory factory; private SqlSession session; private IAccountDao accountDao; @Test public void testFindAll() { //6.执行操作 List<AccountUser> accountusers = accountDao.findAll(); for(AccountUser au : accountusers) { System.out.println(au); } @Before //测试开始前执行 public void init() throws IOException { //1.解析xml配置文件 is = Resources.getResourceAsStream("SqlMapConfig.xml"); //2.构建工厂 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is); //3.工厂生产SqlSession对象 session = factory.openSession(); //session = factory.openSession(true); //设置事物的自动提交,可以不用//session.commit(); //4.创建代理Dao的对象 accountDao= session.getMapper(IAccountDao .class); } @After //测试结束后执行 public void destory(){ //session.commit(); //7.释放资源 if( session != null){ session.close(); } if( is != null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } }
方案2:
使用 resultMap,定义专门的 resultMap 用于映射一对一查询结果。 通过面向对象的包含关系可以得知,我们可以在 Account 类中加入一个 User 类的对象来代表这个账户 是哪个用户的。
-
Account类中加入User对象
package com.liuzeyu.domin; import java.io.Serializable; public class Account implements Serializable{ 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; } @Override public String toString() { return "Account{" + "id=" + id + ", uid=" + uid + ", money=" + money + ", 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; } }
-
修改 AccountDao 接口中的方法
public interface IAccountDao { /** * 查询所有账户,同时获取账户的所属用户名称以及它的地址信息 * @return */ * List<Account> findAll(); * } 注意:第二种方式,将返回值改 为了 Account 类型而没有再继承与Account。 因为 Account 类中包含了一个 User 类的对象,它可以封装账户所对应的用户信息。
-
重新定义 AccountDao.xml 文
<resultMap id="rstAc" type="account"> <id property="id" column="id"/> <result property="uid" column="uid"/> <result property="money" column="money"/> <!--javaType="user"映射后封装的结果集 外键:uid--> <association property="user" column="uid" javaType="user"> <id property="id" column="id"/> <id property="username" column="username"/> <id property="address" column="address"/> <id property="sex" column="sex"/> <id property="birthday" column="birthday"/> </association> </resultMap> <!-- 打印user表数据 --> <select id="findAll" resultMap="rstAc"> SELECT a.*,u.username,u.address FROM account a,USER u WHERE a.`UID`=u.`id`; </select>
-
在 AccountTest 类中加入测试方法
@Test public void testFindAll() { List<Account> accounts = accountDao.findAll(); for(Account au : accounts) { System.out.println(au); System.out.println(au.getUser()); } }
<mark>案例需求2</mark>:一对多查询
本案例查询所有用户信息及用户关联的账户信息。
分析: 用户信息和他的账户信息为一对多关系,并且查询过程中如果用户没有账户信息,此时也要将用户信息 查询出来,我们想到了左外连接查询比较合适
-
编写 SQL 语句
SELECT u.*, acc.id id, acc.uid, acc.money FROM user u LEFT JOIN account acc ON u.id = acc.uid 测试该 SQL 语句在 MySQL 客户端工具的查询结果如下
-
User 类加入 List< Account >
package com.liuzeyu.domin; import java.util.Date; import java.util.List; public class User { private Integer id; private String address; private String username; private String sex; private Date birthday; private List<Account> accounts; public List<Account> getAccounts() { return accounts; } public void setAccounts(List<Account> accounts) { this.accounts = accounts; } @Override public String toString() { return "User{" + "id=" + id + ", address='" + address + '\'' + ", username='" + username + '\'' + ", sex='" + sex + '\'' + ", birthday=" + birthday + '}'; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } 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; } }
-
用户持久层 Dao 接口中加入查询方法
/** * 查询所有用户,同时获取出每个用户下的所有账户信息 * @return */ * List<User> findAll();
-
用户持久层 Dao 映射文件配置
<resultMap id="userMap" type="user"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="address" column="address"/> <result property="sex" column="sex"/> <result property="birthday" column="birthday"/> <collection property="accounts" ofType="account"> <!-- collection 是用于建立一对多中集合属性的对应关系 ofType 用于指定集合元素的数据类型 --> <id property="id" column="id"/> <id property="uid" column="uid"/> <id property="money" column="money"/> </collection> </resultMap> <select id="findAll" resultMap="userMap"> select * from user u left outer join account a on a.uid = u.id; </select> 属性解释: collection 部分定义了用户关联的账户信息。表示关联查询结果集 property="accList" :关联查询的结果集存储在 User 对象的上哪个属性。 ofType="account" :指定关联查询的结果集中的对象类型即List中的对象类型。此处可以使用别名,也可以使用全限定名。
- 测试方法:
@Test public void testFindAll() { //6.执行操作 List<User> users = userDao.findAll(); for(User user : users) { System.out.println("-------每个用户的内容---------"); System.out.println(user); System.out.println(user.getAccounts()); } }
问题:resultMap问题
可见如果再resultMap中的collection下封装account,id的column如果取aid和取id是两种不同的结果,为什么会出现这种情况呢?
原因要result子元素的作用说起:
result 子元素:
◆ property 属性:映射数据库列的实体对象的属性。
◆ column 属性:数据库列名或别名。
1)<mark>取id</mark>:由于和account表的id对应上了,所以是肯定是可以查到结果,但是封装成account对象的时候遇到和user表相同的id,mybatis就直接那它当作account的id进行封装了。
2)==取aid:==由于和数据库的列名没有对应上,又没有取别名,因此是可以最后的时候id没有被封装进去。
3)紧接着我们改一下数据库的字段名:
<mark>取aid:</mark>
测试结果:
所以此时和数据库的account表的字段名对应上了,而且没有和user表的id重复,封装对象成功!!!
- 示例2:用户和角色(多对多查询)
一个用户可以有多个角***r> 一个角色可以赋予多个用户
步骤:- 建立两张表:用户表,角色表
让用户表和角色表具有多对多的关系。需要使用中间表,中间表中包含各自的主键,在中间表中是外键。 - 建立两个实体类:用户实体类和角色实体类让用户和角色的实体类能体现出来多对多的关系各自包含对方一个集合引用
- 建立两个配置文件
用户的配置文件角色的配置文件4、实现配置:
当我们查询用户时,可以同时得到用户所包含的角色信息当我们查询角色时,可以同时得到角色的所赋予的用户信息
- 建立两张表:用户表,角色表
<mark>案例需求1</mark>:查询角色,获取角色下所属的用户信息
实现查询所有用户信息并关联查询出每个用户的角色列表。
从 User 出发,我们也可以发现一个用户可以具有多个角色,这样用户到角色的关系也还是一对多关系。
这样 我们就可以认为 User 与 Role 的多对多关系,可以被拆解成两个一对多关系来实现
-
SQL语句准备
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','女','北京金燕龙'),(43,'小二王','2018-03-04 11:34:34','女','北京金燕龙'),(45,'传智播客','2018-03-04 12:04:06','男','北京金燕龙'),(46,'老王','2018-03-07 17:37:26','男','北京'),(48,'小马宝莉','2018-03-08 11:44:00','女','北京修正'); DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `ID` int(11) NOT NULL COMMENT '编号', `ROLE_NAME` varchar(30) default NULL COMMENT '角色名称', `ROLE_DESC` varchar(60) default NULL COMMENT '角色描述', PRIMARY KEY (`ID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; insert into `role`(`ID`,`ROLE_NAME`,`ROLE_DESC`) values (1,'院长','管理整个学院'),(2,'总裁','管理整个公司'),(3,'校长','管理整个学校'); DROP TABLE IF EXISTS `user_role`; CREATE TABLE `user_role` ( `UID` int(11) NOT NULL COMMENT '用户编号', `RID` int(11) NOT NULL COMMENT '角色编号', PRIMARY KEY (`UID`,`RID`), KEY `FK_Reference_10` (`RID`), CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `role` (`ID`), CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; insert into `user_role`(`UID`,`RID`) values (41,1),(45,1),(41,2);
-
角色实体类Role
public class Role { private Integer roleId; private String roleName; private String roleDesc; private List<User> users; public List<User> getUsers() { return users; } public void setUsers(List<User> users) { this.users = users; } @Override public String toString() { return "Role{" + "roleId=" + roleId + ", roleName='" + roleName + '\'' + ", roleDesc='" + roleDesc + '\'' + '}'; } public Integer getRoleId() { return roleId; } public void setRoleId(Integer roleId) { this.roleId = roleId; } public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } public String getRoleDesc() { return roleDesc; } public void setRoleDesc(String roleDesc) { this.roleDesc = roleDesc; } }
- dao层接口
public interface IRoleDao { //检索表 public List<Role> findAll(); }
- 映射配置文件
<?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.liuzeyu.dao.IRoleDao"> <resultMap id="roleMap" type="role"> <id property="roleId" column="id"/> <result property="roleName" column="role_name"/> <result property="roleDesc" column="role_desc"/> <collection property="users" ofType="user"> <id property="id" column="id"/> <id property="username" column="username"/> <id property="address" column="address"/> <id property="sex" column="sex"/> <id property="birthday" column="birthday"/> </collection> </resultMap> <select id="findAll" resultMap="roleMap"> SELECT u.* ,r.id AS rid ,r.role_name,r.role_desc FROM role r LEFT OUTER JOIN user_role ur ON r.id = ur.rid LEFT OUTER JOIN USER u ON u.id = ur.uid; </select> </mapper>
其中SQL语句的编写是难点。
- 测试方法
@org.junit.Test public void testSelect() throws IOException { //5.使用代理对象执行方法 List<Role> roles = roleDao.findAll(); //6.遍历对象 for (Role role : roles) { System.out.println(role); System.out.println("--------"); System.out.println(role.getUsers()); } }
数据库查询结果:
<mark>案例需求2</mark>:查询用户,获取用户下所属的角色信息
在案例1的基础上对比学习:- 在Role类中新增User属性,并新增getXXX setXXX方法
private List<User> users; public List<User> getUsers() { return users; } public void setUsers(List<User> users) { this.users = users; }
- 映射配置文件
<mapper namespace="com.liuzeyu.dao.IRoleDao"> <resultMap id="roleMap" type="role"> <id property="roleId" column="id"/> <result property="roleName" column="role_name"/> <result property="roleDesc" column="role_desc"/> <collection property="users" ofType="user"> <id property="id" column="id"/> <id property="username" column="username"/> <id property="address" column="address"/> <id property="sex" column="sex"/> <id property="birthday" column="birthday"/> </collection> </resultMap> <select id="findAll" resultMap="roleMap"> SELECT u.* ,r.id AS rid ,r.role_name,r.role_desc FROM role r LEFT OUTER JOIN user_role ur ON r.id = ur.rid LEFT OUTER JOIN USER u ON u.id = ur.uid; </select> </mapper>
- dao接口实现
public List<User> findAll();
- 测试方法
@org.junit.Test public void testU2R() throws IOException { //5.使用代理对象执行方法 List<User> users = userDao.findAll(); //6.遍历对象 for (User user : users) { System.out.println(user); System.out.println("--------"); System.out.println(user.getRoles()); } }
数据库查询结果:
4. 扩展知识:JNDI
-
JNDI概述和原理
- 概述:JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口.
- JNDI的原理就是模仿windows系统的注册表,内部数据的存在形式是Map,windows注册表就是一个Map的结构
- JNDI的底层是一个HashMap结构,将名称和对象使用键值对一一对应,这种关系的搭建与Map体系紧紧相连。
- Windows 系统注册表和Tocat服务器的数据存放对比
-
JNDI搭建maven的war工程
- 新建一个maven 工程, 选择web骨架
- 讲将原来mybatis的类和方法导入
-
再webapp目录下新建META-INF文件夹,将context.xml(相当于jndi得配置文件)导入
<?xml version="1.0" encoding="UTF-8"?> <Context> --> <Resource <!-- jdbc/eesy_mybatis可以随便取值(key)--> name="jdbc/eesy_mybatis" <!-- type 存入得对象(value)--> type="javax.sql.DataSource" <!-- --作者是一个tomcat容器 --> auth="Container" maxActive="20" maxWait="10000" maxIdle="5" username="root" password="809080" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/eesy_mybatis" /> </Context>
-
将主配置文件得JDBC连接池部分修改为
<!-- JDBC连接池 --> <dataSource type="JNDI"> <!-- java:comp/env/jdbc/ 路径固定写法 ;name:eesy_mybatis--> <property name="data_source" value="java:comp/env/jdbc/eesy_mybatis"/> </dataSource>
-
测试test 得findAll方法,发现抛出异常
-
使用JNDI数据源
配置完tomcat服务器后
在webapp下得index.jsp添加Java代码:<% InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(is); SqlSession sqlSession = factory.openSession(); IUserDao dao = sqlSession.getMapper(IUserDao.class); List<User> users = dao.findAll(); for (User user : users) { System.out.println(user); } is.close(); sqlSession.close(); %>
-
在浏览器访问:localhost/day04/index.jsp
可以正常使用JNDI连接
- 解释说明
为什么5会抛出异常,而使用index.jsp界面访问却不会?
因为这是一个tomcat项目,.jsp文件会被编译成成.class,然后被tomcat运行,因为jsp本质上就是一个servlet,运行与tomcat容器中。
而一个findAll测试类,并不会被tomcat解析,因为它既不是servlet,也不是jsp,因此会抛出上述异常。