Java企业级分布式架构师010期课程第一章-第一讲-学习笔记
前言
本文和本文内一切内容,均来自我对于开课吧Java企业级分布式架构师010期课程的学习笔记,并在我自身的理解上整理而成.
此外我的学习经验为,视频课程提供主线思维,详细操作自己查博客,只看视频不行。自己根据自己的节奏学习完毕后再看视频,看看自身的知识点是否全部掌握,如果可以就下一课,如果不够就继续自己查自己学。
并且不要急,课上讲的有些东西,可能要花10倍以上时间去练习,这是值得的。
第一章mybatis第一讲-讲解mybatis的基础和高级应用
Mybatis简介
Mybatis是一个持久层1框架,封装了JDBC代码,在开发过程重用于简化那些重复的动作,并且起到规范的处理一些系统重要事物的作用,另外由于多人使用,在传递和整理上更为简便易于维护。
持久层、业务层、表现层是常用的软件开发3层结构。
表现层:springMVC
业务层:spring
持久层:mybaits
Mybatis初步使用
(我现有案例是springmcv架构,mybaits得重新构建一个。也可以考虑整合两个架构,但是初步分析得出需要有较高的熟练度,因此我进行一次本地搭建项目)
编写流程
1, 全局配置文件SqlMapconfig.xml
2, 映射文件 xxxMapper.xml
3, dao代码/mapper代码(接口API)
4, POJO类(传递参数和结果对象)
5, 测试类
初步准备
首先创建一个工程(如果有就不必创建)
并且还需要有一个可以登录的数据库(没有数据库要这个框架干什么)
maven工程建立(eclipse):
选择新建maven project
选择适合自身的模板
创建包名
添加maven依赖
在pom.xml文件中添加servlet-api依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
右击项目名–>maven -update project —>勾选force update,然后-确定
当前目录状态的结果如下:
==发现问题 ==
从结构上发现没有java文件(src/main/java 文件夹)
解决方案
右击项目—>properties–>java build path
选中然后先点击右侧的-移除,再点击-add Folder,
在main路径下创建新文件夹:
填写文件夹名为java,并点击确定。
成果如下:
可以开始编程了
添加mybatis相关依赖
但是开发mybatis还需要导入一些包:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
编写全局配置文件SqlMapconfig.xml
在src/main/resources文件夹中,创建Mybatis的主配置文件SqlMapConfig.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="mysql">
<!--配置mysql的环境-->
<environment id="mysql">
<!--配置事务的类型-->
<transactionManager type="JDBC"></transactionManager>
<!--配置连接池-->
<dataSource type="POOLED">
<!--配置连接数据库的4个基本信息-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3307/cloudserver"/>
<property name="username" value="root"/>
<property name="password" value=""/>
</dataSource>
</environment>
</environments>
<!--指定映射配置文件的位置,映射配置文件指的是每个dao独立的配置文件-->
<mappers>
<mapper resource="com/bao/mybatisTest/mapper/UserMapper.xml"/>
</mappers>
</configuration>
目的是为了连接这个数据库,本次以操作user表为例子:
映射文件 xxxMapper.xml
UserMapper.xml
根据SqlMapconfig.xml里的设置放置在路径(或者是先创建映射文件然后再同步路径):
resource->com->bao ->UserMapper.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">
<!--namespace:用来区别不同的类的名字 -->
<mapper namespace="baotest">
<!-- 通过Id查询一个用户 -->
<select id="findUserById" parameterType="Integer" resultType="com.ben.domain.User">
select * from user where id = #{v}
</select>
</mapper>
核心条目:
<!-- 通过Id查询一个用户 -->
<select id="findUserById" parameterType="Integer" resultType="com.bao.domain.User">
select * from user where id = #{v}
</select>
注意此处的关键细节:
1, id="findUserById" (唯一标识,类似方法名)
2, namespace="baotest" (类似包名)
3, select (表示查询)
4, parameterType="Integer" (输入参数类型)
5, resultType="com.bao.domain.User" (类型结构来自这个实体类)
6, * (返回全部信息)
7, #{v} (绑定输入,当基本类型或string时,填什么无所谓,当为pojo类型时,必须和类型一致)
8, from user where (从user表中查询)
后续在dao实现类中进行对接。
dao代码/mapper代码(接口API)
我对于dao和mapper不太了解,但是以我的认识,我应该是采用了mapper的方式,但是课件里都称呼为dao。(解决疑惑后我明确我使用的方法为dao方法)
存疑:dao和mapper的区别?
代码:
接口文件
package com.bao.mybatisTest.mapper;
import com.bao.mybatisTest.domain.User;
public interface UserMapper {
/**
* find user by user id
* @param id
* @return
*/
User findUserById (int id);
}
代理文件
package com.bao.mybatisTest.mapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSession;
import com.bao.mybatisTest.domain.User;
public class UserDaoImpl implements UserMapper{
@SuppressWarnings("unused")
private SqlSessionFactory sqlsessionFactory;
//注入sqlsessionFactory
public UserDaoImpl(SqlSessionFactory sqlsessionFactory) {
this.setSqlsessionFactory(sqlsessionFactory);
}
public void setSqlsessionFactory(SqlSessionFactory sqlsessionFactory) {
this.sqlsessionFactory = sqlsessionFactory;
}
public User findUserById(int id) {
//sqlsessionFactory工具类创建sqlsession会话
SqlSession sqlSession= sqlsessionFactory.openSession();
//sqlsession 接口,借此进行数据库操作
User user = sqlSession.selectOne("baotest.findUserById",id);
//释放资源
sqlSession.close();
return user;
}
}
dao和mapper的区别
mapper方法是dao方法的升级版,编写步骤类似,但是省去了Dao实现类。
在dao方法中, 开发者需要编写DAO接口类和DAO实现类
需要向DAO实现类中注入 SqlSessionFactory,在方法体内通过 SQLSessionFactory创建 SQLSession
而在mapper方法中,开发者需要编写mapper.xml映射文件类
开发者需要mapper接口 需要遵循一些 开发的规范,mybatis可以自动生成mapper实现类的代理对象
因此,我以上代码开发方式为dao方式,后续可以优化为mapper方式
修改为:
<mapper namespace="com.bao.mybatisTest.UserMapper">
<!-- 通过Id查询一个用户 -->
<select id="findUserById"
parameterType="Integer"
resultType="com.bao.mybatisTest.domain.User">
select * from user where UserID = #{v}
</select>
</mapper>
POJO类(传递参数和结果对象)
课件里貌似没说,先跳过
什么是pojo类?
什么是SqlsessionFactory?
存疑。
pojo类
实体类,比如User。后续有讲
SqlsessionFactory
3个要素:SqlSessionFactoryBuilder、SqlsessionFactory、SqlSession
通过 SqlSessionFactoryBuilder创建会话工厂 SqlSessionFactory
通过 SQLSessionFactory创建 SqlSession
- SqlSession是mybatis库里提供的一个接口,提供了很多操作数据库的方法,用于调用dao接口;但是SqlSession线程不安全,最好的应用场景为方法内作为局部变量被调用。
- SqlSessionFactory可以单例管理(只创建一次,之后持续使用)
- SqlSessionFactoryBuilder不需要单例管理,需要使用时直接new
测试代码
简单测试一下:
package com.bao;
import java.io.IOException;
import java.io.InputStream;
import com.bao.mybatisTest.domain.User;
import com.bao.mybatisTest.mapper.UserDaoImpl;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class Test {
public static void main(String[] args) {
try {
testSearchById();
} catch (IOException e) {
e.printStackTrace();
}
}
//通过Id查询一个用户
public static void testSearchById() throws IOException {
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
//3.使用工厂创建接口类对象
UserDaoImpl userDaoImpl = new UserDaoImpl(sqlSessionFactory);
//4.调用接口方法
User user = userDaoImpl.findUserById(1147);
//5. 打印结果
System.out.println(user.getName());
in.close();
}
}
打印如下:
111
对应的数据库条目:
运行成功并且数值正确,至此完成本期任务以Mybatis架构操作数据库
mapper代理开发方式
dao方法目前已经相对落后了,因此采用更为简便的mapper方式进行开发。
代理理解
代理分为静态代理和动态代理,mybaits中使用的代理方式为动态代理。
而动态代理又分两种方式 :
基于JDK和基于CGLTB.
区别在于针对有接口的类采用JDK,而针对子类继承的方式采用CGLTB
mybaits支持这2种方式,但是mapper方式使用jdk方式进行代理
XML方式
很方便,
开发时只需要编写mapper接口(java)和mapper映射文件(xml),不需要实现类
开发时需要遵循开发规范,总共有4条(后3条跟dao一样):
- mapper接口路径和映射文件中的namespace相同(关键)
- mapper接口方法名和映射文件定义的statement的id相同(跟dao一样)
- 输入参数类型一致(跟dao一样)
- 返回参数类型一致(跟dao一样)
后3条简单记:(输入输出和方法名一致)
测试代码
修改映射文件为:
<mapper namespace="com.bao.mybatisTest.mapper.UserMapper">
<!-- 通过Id查询一个用户 -->
<select id="findUserById"
parameterType="Integer"
resultType="com.bao.mybatisTest.domain.User">
select * from user where UserID = #{v}
</select>
</mapper>
修改test类新增方法
//通过Id查询一个用户,mapper方式
public static void testSearchById2() throws Exception {
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
//3.创建UserMapper对象
SqlSession sqlSession= sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//4.调用UserMapper对象的API
User user = userMapper.findUserById(1147);
//5. 打印结果
System.out.println(user.getName());
//6. 释放资源
sqlSession.close();
in.close();
}
测试完毕结果一致
注解方式
更简单
只需要编写mapper接口文件
但是课上没详讲,根据课件摸索一下
讲师表示,不建议采用这种方式,因为开发原则不建议频繁修改持久层代码,因此这部分内容应写入配置文件用xml方式代理。
我觉得合理。
测试中:
发现故障:
is not known to the MapperRegistry
研究后得出
课件里没讲一个操作(可能我漏看了)需要在SqlMapconfig.xml里指定映射配置文件(类)。
如下:
<mappers> <mapper class="com.bao.mybatisTest.mapper.AnnotationUserMapper"></mapper>
</mappers>
测试代码
//通过Id查询一个用户注解方式
public static void testSearchById3() throws Exception {
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
//3.创建UserMapper对象
SqlSession sqlSession= sqlSessionFactory.openSession();
AnnotationUserMapper userMapper = sqlSession.getMapper(AnnotationUserMapper.class);
//4.调用UserMapper对象的API
User user = userMapper.findUserById(1147);
//5. 打印结果
System.out.println(user.getName());
//6. 释放资源
sqlSession.close();
in.close();
}
成功。
全局配置文件
什么是全局配置文件,不知道。课上没详细讲,初步认识是,名称为SqlMapConfig.xml的文件。
找了一下,发现自己有写:
在这个文件里的标签有且只能按如下顺序排列
- properties(属性)
- settings(全局配置参数)
- typeAliases(类型别名)
- typeHandlers(类型处理器)–Java类型–JDBC类型—> 数据库类型转换
- objectFactory(对象工厂)
- plugins(插件)–在Mybaits执行SQL语句的流程中,插入实现一些功能增强的效果,比如PageHelper分页插件(这是什么,没用过,以后研究)
- environments(环境集合属性对象)
-
- environment(环境子属性对象)
-
-
- transactionManager(事务管理)
-
-
-
- dateSource(数据源)
-
- mappers(映射器)
在我目前的测试工程里,只使用了environments和mappers里的功能和子标签
但是这些不重要,重点关注这3个:
properties、typeAliases和mappers
剩余的
settings,在后面课程里的缓存和延时加载时会有使用
properties标签
用于加载数据库的相关信息,我之前是采用直接赋值的方法配置,但是这种方式在后期维护上就显得不太方便,因此可以采用property标签来进行优化。
总体分两步,
第一步在classpath下定义db.properties文件
放在这里。
然后输入相关参数:
#sql
jdbc.driver = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://127.0.0.1:3307/cloudserver
jdbc.username = root
jdbc.password =
再修改文件
SqlMapConfig.xml里对应的标签为:
<dataSource type="POOLED">
<!--配置连接数据库的4个基本信息-->
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
测试一下,bug了:
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.sql.SQLException: Error setting driver on UnpooledDataSource. Cause: java.lang.ClassNotFoundException: Cannot find class: ${jdbc.driver}
### The error may exist in com/bao/mybatisTest/mapper/UserMapper.xml
### The error may involve com.bao.mybatisTest.mapper.UserMapper.findUserById
### The error occurred while executing a query
咋回事呢,发现漏了一句:<properties resource = "db.properties"/>
还是有错:Could not find resource db.properties
研究后发现,对于maven方式创建的工程,默认的resource路径放在:
main中的resources目录里,而不是总目录,因此修改如下:
测试运行,运行成功。
以上就是properties标签功能的简单运用。通过加载对应文件的内容来设置参数。此外还可以直接设置子标签内的参数(原本就是这么写的,不做演示了)
总结,properties标签的功能在于以key-value的方式去加载数据
此外,细节:
properties标签配置数据时,优先读取直接设置的参数,再读取配置文件里(以url或resource方式)的参数,而在遇到参数名相同的情况时,后读取的会覆盖先读取的值。(简单的说就是配置文件里的可以覆盖直接定义的值)
另外还会受到到系统环境同名变量的干扰(具体怎么冲突没说)因此在.properties文件里定义变量时,需要加入前缀,防止同名(例如:jdbc.username
)。
typeAliases标签
作用:用于简化映射文件(mapper.xml)中parameterType和resultType
默认支持表。(略)
使用这个标签进行进化(举例)
(出了bug:元素类型为 "configuration" 的内容必须匹配 "(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?
,这是我放错位置导致的,在全局配置文件里,标签的顺序是固定的)
<typeAliases>
<!-- 单个定义 -->
<typeAlias alias= "m_User" type="com.bao.mybatisTest.domain.User"/>
<!-- 群体定义 -->
<package name="com.bao"/>
</typeAliases>
此时映射文件里的语句可以改为:
<!-- 通过Id查询一个用户 -->
<select id="findUserById"
parameterType="Integer"
resultType="m_User">
select * from user where UserID = #{v}
</select>
在上述修改中,演示了单体定义的效果,
而群体定义的规则为:将包名下的所有类生成,类名首字母小写的别名,例如将com.bao.User,自动生成user。当我定义<package name="com.bao"/>
时(效果不演示了)
mappers标签
总共有4种用法
- resource (一般用法)
- class (注解用法,额外需要:映射文件和接口文件名称相同且在同一目录下)
- url (绝对路径用法,通常弃用)
- package (一般用法的批量版本,常用,额外需要:映射文件和接口文件名称相同且在同一目录下)
代码示例:
以下定义的是同一个文件(但是不要同时用多种方式定义同一文件,以下仅做演示)
<mappers>
<mapper resource="com/bao/mybatisTest/mapper/UserMapper.xml"/>
</mappers>
<mappers>
<mapper class="com.bao.mybatisTest.mapper.AnnotationUserMapper"/>
</mappers>
<mappers>
<package name="com/bao/mybatisTest/mapper"/>
</mappers>
测试时发现:由于package方法可以包含同类型下的resource方法和class方法,所以不能共存,但是resource和class独立,可以共存。由于package方式可以批量的添加映射文件,所以常用这种方式。
映射文件
输入映射parameterType
parameterType属性可以映射的输入参数java类型有:
简单类型、POJO类型(啥意思?自己定义的类型吗,有空解惑)、Map类型,List类型(数组)
Map类型和POJO类型用法类似,因此主讲POJO
List类型在后续动态SQL部分进行讲解
简单类型
比如,int型,简单,略
简单类型的进阶使用
<!-- 通过名称模糊查询用户 -->
<select id="findUserByName"
parameterType="String"
resultType="m_User">
select * from user where UserName like '%${value}%'
</select>
当查询结果返回值为一个list时,只需要关注list内的对象类
当使用String格式模糊查询时候,会需要使用${}
,因为如果使用#{}
则系统会在转换参数时,再额外加入一对单引号
比如输入为李四时,合成的查询语句就会变成:
select * from user where UserName like '%'李四'%'
这显然不是所需要的
而${}
则不会额外加入内容。
使用${}
这种方式,需要满足2个条件,
- 参数名为value
- 并且输入参数为简单类型
POJO类型
需要将pojo类型设置为输入参数的情况,通常是向sql内添加数据,在这种情况下,需要使用标签:insert
这部关键的点在于从自定义的POJO类中,取出需要的参数进行SQL语句拼合
<insert id="insertUser"
parameterType="m_User">
<selectKey keyProperty = "Userid" order ="AFTER" resultType="Integer" >
select LAST_INSERT_ID()
</selectKey>
insert into user(Userid,RegNo,Name,State)
values(null,#{RegNo},#{Name},#{State})
</insert>
在查询语句中添加了一份主键返回的功能,其中的selectKey和后面的insert语句的先后顺序由参数 order来决定,此时就是先insert再selectKey
selectKey的返回值需要来自于pojo类里定义的属性
在insert语句中,必须使用#{}而且参数名也必须和POJO里的类名一致,也就是3处一致
LAST_INSERT_ID() 是mysql里的一个库函数
OGNL
- #{}:是通过反射数据的—StaticSQLSource
- ${}:是通过OGNL表达式会随着对象的嵌套而相应的发生层级变化(啥意思,以后再研究)–DynamicSQLSource
对象导航语言(用到了再学,先做笔记)
|---User(参数值对象)
|--userName--张三
|--birthday
|--dept – Department
|--name
通过OGNL表达式获取Department属性:dept.name
理解:获取对象下属一级属性,直接输入属性名,获取二级属性时,则是一级属性名点二级属性名:A.B;
POJO嵌套类型
课程不详细讲,使用方法很简单当需要用子类的数据而输入参数是父类时,用OGNL表达式从父类对象中取参数。
一般不常使用,用到时要能够理解
输出映射resultType和resultMap
resultType
resultType可以映射的类型有:简单类型,POJO类型、Map类型
其中Map类型和POJO类型类似,因此省略
使用resultType进行输出映射时,要求sql语句中查询的列名要和映射的pojo的属性名一致
简单举例:
<!-- 通过Id查询一个用户名 -->
<select id="findUserNameById"
parameterType="Integer"
resultType="String">
select name from user where UserID = #{v}
</select>
public String findUserNameById (int id) throws Exception;
注意输出简单类型时,必须查询出的结果集只有一列
resultMap
resultMap存在的目的在于满足查询Sql的查询类名和pojo的属性名不一致的情况。其通过将列名和属性名作一个对应关系,然后将查询结果映射到指定的pojo对象中,而且可以支持输出多列结果集
在底层下resultTpye是用resultMap实现的,所以是更为基础的应用方式
同样的也更为繁琐
总体分为两步:
首先建立对应表
<resultMap type="m_User" id="userListResultMap">
<!--
id标签:查询结果的主键列
result标签:查询结果的普通列
column:查询sql的列名
property: POJO类的属性名
-->
<id column = "id_" property ="userid"/>
<result column = "name_" property ="name"/>
</resultMap>
其次建立语句
<select id="findUserListResultMap"
resultMap="userListResultMap">
SELECT userid id_,name name_ FROM user
</select>
注意,使用这种方法需要单独建立resultMap标签,定义对应关系
后续学延迟加载的时候,也是用这种方法实现
#{}和${}的区别
- 区别1
#{} 相当于JDBC SQL 语句中的占位符:?(PrepareStatement)
${} 相当于JDBC SQL 语句中的连接符:合 +(Statement) - 区别2
#{} 在输入映射时会对参数进行解析(主要是String类型会自动加单引号)
${} 直接输入参数,不做处理 - 区别3
#{} 在进行对简单类型的输入映射时,大括号中的参数名可以任意
${} 在进行对简单类型的输入映射时,大括号中的参数名只能是value - 区别4
${} SQL注入会出现问题,使用 OR 1=1 关键字将查询条件忽略(啥意思??)
解惑POJO和${} SQL注入问题
POJO
POJO(Plain Old Java Objects)是简单的Java对象,实际就是bai普通JavaBeans,是为了避免和EJB混淆所创造的简称。
理解:自定义的实体类,比如我的:User
${} SQL注入问题
简单解释:由于是直接输入参数,因此当输入的数据内容可以被组合成SQL语句时,将破坏原本的语句结构,造成不可预计的后果,因此${}建议只采用在String类型输入时使用
如果被迫进行风险编程,则需要在java层进行调用时,更具具体情况做出防御。
关联查询(从这里往下知识变难)
存在表A,和表B存在关联关系,比如两个表里都有用户ID并且指定的用户相同
视屏中举例是在电商等购物网站上下的订单号对应一个客户,而一个客户可以下多个订单
此时存在表A,存放所有用户创建的订单,包含订单号(id)和创建订单的用户(user_id)(外键)等,
存在表B,存放所有下订单的客户信息,包含用户id号(user_id)和其他信息等
(所谓外键就是其他表的主键,可以以此建立关联关系,是吗?)
- 那么从表A以单号或者其他条件进行查询时就能获取user_id条目,以这个条目(外键)进行另一个表的信息获取,就是一对一的关联查询。
- 反过来的话就是可能一对多的关联查询,因为存在一个客户下了多个单的可能性
(此处我认为,一对多方法是可以包含一对一查询的,也就是一对一查询法可以理解为关联条件为外键时才能使用,但是有一点我存疑,能不能只是普通条目就进行一对多的关联查询,如果有空验证一下。)
(为了方便演示,我搭建了2个表,作为实验对象)
表A:test_order
表B:test_user
一对一关联查询
需求查询所有订单信息,并且关联下单的用户信息。
首先准备SQL语句:
SELECT
test_order.order_id,
test_order.user_id,
test_order.buy_info,
test_user.name
FROM
test_order LEFT JOIN test_user
ON test_order.user_id = test_user.user_id
主信息:订单信息
从信息:用户信息
方法一:resultType
学生自己实现(课后我研究一下)
方法二:resultMap
使用resultMap进行结果映射,定义专门的resultMap 用于映射一对一查询结果
首先准备扩展po类:
public class OrderEx extends TestOrder{
private TestUser testUser;
public TestUser getTestUser() {
return testUser;
}
public void setTestUser(TestUser testUser) {
this.testUser = testUser;
}
}
用于封装结果对象,由于是1对1因此只使用单个TestUser对象存储关联查询的用户信息
然后准备Mapper映射文件
<resultMap type="OrderEx" id="ordersAndUserRstMap">
<!-- 方便起见,我采用了相同的名建立对应关系 -->
<id column = "order_id" property ="order_id"/>
<result column = "user_id" property ="user_id"/>
<result column = "buy_info" property ="buy_info"/>
<!-- TestUser用了全局配置文件群体定义的别名
testUser 则是封装类里定义的变量-->
<association property="testUser"
javaType = "TestUser">
<id column = "user_id" property ="user_id"/>
<result column = "name" property ="name"/>
</association>
</resultMap>
<select id="findOrdersAndUserRstMap"
resultMap="ordersAndUserRstMap">
SELECT
o.order_id,
o.user_id,
o.buy_info,
tu.name
FROM
test_order o
JOIN `test_user` tu ON tu.user_id = o.user_id
</select>
准备接口函数
public List< OrderEx > findOrdersAndUserRstMap() throws Exception;
完毕测试结果:
//4.调用Mapper对象的API
List<OrderEx> orders= mapper.findOrdersAndUserRstMap();
//5. 打印结果
System.out.println(orders.toString());
打印信息如下
[OrderExTestOrder [order_id=1, user_id=1, buy_info=apple]
[testUser=TestUser [user_id=1, name=bob]
]
, OrderExTestOrder [order_id=2, user_id=1, buy_info=iphone]
[testUser=TestUser [user_id=1, name=bob]
]
, OrderExTestOrder [order_id=3, user_id=2, buy_info=banana]
[testUser=TestUser [user_id=2, name=edison]
]
, OrderExTestOrder [order_id=4, user_id=2, buy_info=light]
[testUser=TestUser [user_id=2, name=edison]
]
]
和自身的表对上,合理
研究一下resultType的方法
研究了一段时间一度遇到困境,虽然能够获取order表里的信息,但是无法把信息传入OrderEx类里的TestUser
比如:OrderExTestOrder [order_id=4, user_id=2, buy_info=light] [test_user=null]
深入摸索后,得出结论,
在sql查询语句的运行情况中,是不返回嵌套值的
因此修改如下,
修改封装类:
public class OrderEx extends TestOrder{
private int user_id;
private String name;
public int getUser_id() {
return user_id;
}
public void setUser_id(int user_id) {
this.user_id = user_id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "OrderEx"+super.toString()
+ "test_user [user_id=" + user_id + ", name=" + name + "]\n";
}
}
使得满足resultType传参数的条件:返回列名必须和POJO类的变量名一致
其次创建映射语句:
<select id="findOrdersAndUserRstType"
resultType="OrderEx"
>
SELECT
*
FROM
test_order o,test_user tu
WHERE
tu.user_id = o.user_id
</select>
这种查询摸索和关联查询效果相同,不清楚实际使用时采用什么方案。
运行结果:
[OrderExTestOrder [order_id=1, user_id=0, buy_info=apple]
test_user [user_id=1, name=bob]
, OrderExTestOrder [order_id=2, user_id=0, buy_info=iphone]
test_user [user_id=1, name=bob]
, OrderExTestOrder [order_id=3, user_id=0, buy_info=banana]
test_user [user_id=2, name=edison]
, OrderExTestOrder [order_id=4, user_id=0, buy_info=light]
test_user [user_id=2, name=edison]
]
与预期相符。
并且此处也发现了sql查询是不返回嵌套值的。
结论
对于复杂情况老实用resultMap,一方面结构更强清晰,另一方面传参更为简单
一对多关联查询
当情况变的更加复杂的时候,就只能使用resultMap(所以老实研究这个,别想做简化,简单模式下就是resultType,其余情况全都用resultMap)
一对多关联查询的情况,通常需要将对象封装,因此resultMap的结构更为合理合适,首先准备SQL语句
SELECT
u.*,
o.order_id,
o.buy_info
FROM
`test_user` u
LEFT JOIN test_order o ON o.user_id = u.user_id
(存疑这种方式和这样的语句的区别是什么,有空试试)
SELECT
u.*,
o.order_id,
o.user_id,
o.buy_info
FROM
`test_user` u , `test_order` o
WHERE
o.user_id = u.user_id
然后扩充po类:
public class UserEx extends TestUser{
private List<TestOrder> orders;
public List<TestOrder> getOrders() {
return orders;
}
public void setOrders(List<TestOrder> orders) {
this.orders = orders;
}
@Override
public String toString() {
return "UserEx "
+ super.toString()
+ "[orders=" + orders + "]";
}
}
最后准备映射文件和接口文件类
<resultMap type="UserEx" id="UsersAndordersRstMap">
<!-- 方便起见,我采用了相同的名建立对应关系 -->
<!-- 用户信息映射 -->
<id column = "user_id" property ="user_id"/>
<result column = "name" property ="name"/>
<!-- 一对多关联映射 -->
<collection property="orders"
ofType = "TestOrder">
<id column = "order_id" property ="order_id"/>
<result column = "user_id" property ="user_id"/>
<result column = "buy_info" property ="buy_info"/>
</collection>
</resultMap>
<select id="findUsersAndOrderRstMap"
resultMap="UsersAndordersRstMap">
SELECT
u.*,
o.order_id,
o.buy_info
FROM
`test_user` u
LEFT JOIN test_order o ON o.user_id = u.user_id
</select>
这里关注一个知识点,一对多关联映射使用标签为:collection
并且 用ofType标注POJO类
(一对一则是javaType,)
其余基本不变。
(我猜想,一对多模式下,应该也能包含一对一的情况,那么能不能只记忆一对多的写法呢。于是我认为在底层源码上,一对一可能效率更高。)
运行语句(前后略)
List<UserEx> userEx= mapper.findUsersAndOrderRstMap();
//5. 打印结果
System.out.println(userEx.toString());
结果:
[UserEx TestUser [user_id=1, name=bob]
[orders=[TestOrder [order_id=1, user_id=1, buy_info=apple]
, TestOrder [order_id=2, user_id=1, buy_info=iphone]
]], UserEx TestUser [user_id=2, name=edison]
[orders=[TestOrder [order_id=3, user_id=2, buy_info=banana]
, TestOrder [order_id=4, user_id=2, buy_info=light]
]]]
另一种写法的结果:
[UserEx TestUser [user_id=1, name=bob]
[orders=[TestOrder [order_id=1, user_id=1, buy_info=apple]
, TestOrder [order_id=2, user_id=1, buy_info=iphone]
]], UserEx TestUser [user_id=2, name=edison]
[orders=[TestOrder [order_id=3, user_id=2, buy_info=banana]
, TestOrder [order_id=4, user_id=2, buy_info=light]
]]]
一致(所以我不懂二者的区别,等我境界高了再研究)
后续研究,根据后续课程延迟加载的定义,我认为第二种查询方式可能是直接查询无法实现延迟查询。真的吗?存疑有空研究。
结论
一对多模式只能使用resultMap做返回属性定义,并且内部封装类要用
collection标签。
(不难,我学会了)
延迟加载
定义
(抄课件)
Mybatis中的延迟加载,也称为懒加载,是指在进行关联查询时,按照设置延迟规则推迟对关联对象的select查询,延迟加载可以有效的减少数据库压力。(说明优势)
Mybatis的延迟加载,需要通过resultMap标签中的association和collection子标签才能延时成功(为什么?这个两个标签不是关联查询时用的吗)
Mybatis的延迟加载,也被称为是嵌套查询,对应的还有嵌套结果的概念,可以参考一对多关联的案例(这么看来对于下层来说可能是resultMap标签一种功能的不同执行方式)
注意:MyBaits的延迟加载只是对关联对象的查询有延迟设置(所以用resultMap?),对于主加载对象都是直接执行查询语句的
分类
Mybatis根据对关联对象查询的select语句的执行时机,分为3种类型:直接加载、侵入式延迟加载与深度延迟加载
- 直接加载:执行完主对象的查询,马上进行关联对象的查询(也就是不延迟)
- 侵入式延迟加载:执行对主加载对象的查询时,不会执行对关联对象的查询,但是要访问主加载对象的某个属性时(该属性不是关联对象的属性,此处我猜想是应该是任何属性的意思,有空研究),就会马上执行关联对象的查询
- 深度延迟加载:执行对主加载对象的查询时,不会执行对关联对象的查询。只在访问真正的关联对象的详情时,才会进行对关联对象的查询
(这3个层次就是3种关联查询的启动标准,直接,访问时,访问关联对象时。我是这么认为的)
延迟加载策略需要在Mybaits的全局配置文件(SqlMapConfig.xml)中,通过<settings>
标签进行设置。
直接加载
配置参数:lazyLoadingEnabled = false;
相当于没有配置。不配置就是直接加载
<settings>
<setting name="lazyLoadingEnabled" value="false"/>
</settings>
侵入式延迟加载
配置参数:lazyLoadingEnabled = true;
配置参数:aggressiveLazyLoading = true;
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="true"/>
</settings>
深度延迟加载
配置参数:lazyLoadingEnabled = true;
配置参数:aggressiveLazyLoading = false;
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
案例准备
查询订单信息以及他的下单用户信息。
测试表A和B
[UserEx TestUser [user_id=1, name=bob]
[orders=[TestOrder [order_id=1, user_id=1, buy_info=apple]
, TestOrder [order_id=2, user_id=1, buy_info=iphone]
]], UserEx TestUser [user_id=2, name=edison]
[orders=[TestOrder [order_id=3, user_id=2, buy_info=banana]
, TestOrder [order_id=4, user_id=2, buy_info=light]
]]]
运行了一下,没发现什么变化,这种优化的手段需要特殊的情况下才能发现,在这种简单的测试环境里,看不出差别,
学完了。
N+1问题(拓展研究)
延迟加载的目的是为了减轻数据库压力,但是在部分情况下可能会增大对数据库的压力
当延迟加载的表数据太多时,由于采用延迟加载测量,因此从表的信息查询会每一条被访问的数据,进行多次查询。
这种情况下应该怎么办?如果出现这种设计场景,应该尽力采用单次查询的方法。
Mybatis缓存
介绍
Mybatis提供查询缓存,如果缓存中有数据就不用从数据库中获取,用于减轻数据压力,提高系统性能。
Mybatis的查询缓存有2级,称为一级缓存和二级缓存
- 一级缓存是SQLSession级别的缓存,操作数据时生成SQLSession对象时会生成一个数据结构(HashMap)用于存储缓存数据,不同对象间数据区域不共享(因此SqlSession对象不使用时需要释放空间)
- 二级缓存是Mapper(namespace)级别的缓存,多个sqlSession对象去操作同一个Mapper的sql语句时(同一个),可以共用二级缓存。
二级的范围比一级大。但是二级不太好用,当sqlSession进行数据库变化的操作时,缓存区域也要进行清除,以确保最新的数据能够被获取,因此由于二级缓存范围过大,因此在场景为有变化操作的环境里,会频繁的清除缓存。一级也有同样的问题,但是范围小所以,影响不大
一级缓存
一级缓存是默认开启的,怎么关没有讲,可以认为必然使用
运行逻辑为
查询时,先从缓存里查询,如果没有就从数据库查询,把结果存入一级缓存
当sqlsession执行变化操作时(增删改),清除缓存
演示:
略,自己想想。反正就是进行查找时如果缓存里有就不去数据库里找,如果没有就去。另外根据需求进行缓存更新。
二级缓存
二级缓存是默认不开启的,想要开启需要操作核心配置文件
添加setting条目:
<setting name="cacheEnabled" value="true"/>
然后在想要使用二级缓存的mapper文件添加:
<cache></cache>
并且还要为二级缓存的目标实现序列化(准备一个实例)
为需要缓存的实体类,实现序列化(如果该类有父类,则父类也需要序列化)
implements Serializable
(我之前创建的都有序列化,原来是这个意思)
演示:
略。
禁用二级缓存和刷新二级缓存
略。
应用场景
要求较高的响应速度,但是实时性要求不是很高时。
注意,使用二级缓存时还需根据需求设置刷新间隔,详细操作略。
局限性
二级缓存的缺点之前也说了,如果目标众多,而单一目标的修改需要清除全部缓存,比较浪费。
对此可以有解决方案,就是确保缓存对象不会被变化,将需要变化或者可能的变化的对象,放入另一个映射文件去处理
动态SQL(重点知识)
目的:完成字符串的拼接处理、循环判断。
(sql的逻辑处理)
解决的问题:
- 在映射文件中,会编写很多有重叠部分的SQL语句
- SQL中where条件有多个,但是页面参数不确定
总结:用逻辑来处理和简化问题
这里我提出一个问题:为什么不用java逻辑来实现简化问题,我认为逻辑方面还是交给逻辑代码更合适,求解。
if标签
例如处理用户名问题
如果是数值可以判断是什么数
如果是字符传可以判断是否为空,通常包括null和0长度字符串情况
演示代码
<select id="findUserByName"
resultType="TestUser"
parameterType = "ALL_User"
>
SELECT
*
FROM
test_user
WHERE
1=1
<if test="m_User != null">
<if test="m_User.name != null and m_User.name != ''">
AND name like '%${test_user.name}%'
</if>
</if>
</select>
这段演示代码的入参的结构为ALL_User.m_User.name(OGNL)依次判断里面的m_User对象然后继续拼接
当这些if标签内的test的属性语句运算为真时才会允许拼接下面的
而此处where
后的1=1
目的是为了保证语法的正确性,因为sql语句可能拼接,也可能不拼接,设计时需要注意。
但是这种写法会让sql执行是多一个步骤,而用上where标签则可以优化这个问题
where标签
使用where标签后相同功能的代码可以改写为这样:
<select id = "findUserByName2"
resultType="TestUser"
parameterType = "ALL_User"
>
SELECT
*
FROM
test_user
<!-- where标签相当于where并且会自动处理后面连接的AND -->
<where>
<if test="m_User != null">
<if test="m_User.name != null and m_User.name != ''">
AND name like '%${test_user.name}%'
</if>
</if>
</where>
</select>
(存疑,上述的写法我能不能手动把AND去掉呢)
验证后得出可以去掉,但是有不影响运行,这里有AND的目的为了方便可能的连续的if标签组使用
结论:if标签建议配上where标签使用
SQL片段
在某些情况下,多个sql语句的部分内容是完全相同的,例如在多种情况下查询同一个表,查的表和一些条件是相同的,如果重复的写,会使得映射文件篇幅较大。
因此可以使用SQL片段的方式来进行省略。
(这个功能和C语言中的宏定义函数块类似,都是以某种方式,让简单化的信息代替重复的复杂信息。)
使用方法如下
举例:
<sql id="equal_name">
<if test="m_User != null">
<if test="m_User.name != null and m_User.name != ''">
AND name like '%${test_user.name}%'
</if>
</if>
</sql>
<select id="findUserByName3"
resultType="TestUser"
parameterType = "ALL_User"
>
SELECT
*
FROM
test_user
<where>
<include refid="equal_name"></include>
</where>
</select>
用法:
在定义时用sql标签,在使用是用include标签
当引用其他映射文件里的片段时需要在前面加上文件名,例如
当其他文件引用上文定义的片段时,include标签要这么写:
<include refid="com.bao.mybatisTest.mapper.TestMapper.equal_name"></include>
foreach标签
表示遍历,当使用的输入映射里有个集合时,但是需要对集合内的每一个对象进行参数判别来形成查询条件时(比如我抽奖时,需要从库中取得座位号是若干号码的情况,此时我传入一个类中有个id集合表号码)
这种情况下可以又SQL语句来实现:
假定集合的内容是,{5,9,11}
那么最终需要拼合语句就是
where id in (5,9,11)
那么SQL的判别语句用foreach来实现就是如下:
<if test="ids != null and ids.size() > 0">
<foreach collection="ids"
item = "id"
open = " id IN ( "
close = " ) "
separator = ","
>
#{id}
</foreach>
</if>
- collection 表示输入参数的集合名称(输入POJO类中定义的集合元素的名字)
- item 表示声明集合内参数的变量名(这个名称是自定义的,但是需要和下方的入参关联)
- open 表示循环开始时拼接的字符串
- close 表示循环结束时拼接的字符串
- separate 表示循环每执行一步后拼接的字符串
结论:foreach标签可以将入参中的集合拆解,顺次提取出内部的元素,然后根据需求进行字符串拼接
注意:如果,入参不是POJO类而是一个List或者Array时,foreach标签内的collection的属性为对应的List或者Array,包括if语句里的数组名(也就是所有出现的数组名改为List或者Array,因为没有这个概念了)
Mybatis逆向工程工具(略过)
介绍
逆向工程是使用官方网站的Mapper自动生成工具mybatis-generator-core-1.3.2来针对单表生成PO类(Example)和Mapper接口和Mapper映射文件
(我认为我中短期用不到,因此本课简单略过。后续用到了再做专门的说明)
使用方法
- 第一步准备一个工程mybatis-generator去官网下载
- 第二步修改工程的配置文件generatiorConfig.xml
- 第三步运行工程,以java方式
修改配置文件
下载完成模板后,主要需要做的就是修改配置文件
在generatiorConfig.xml中配置Mapper生成的详细信息,配置时主要以下细节:
- 生成的数据库表
- POJO文件所在的包路径
- Mapper所在的包路径
配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<!-- 配置生成器 -->
<generatorConfiguration>
<context id="test_order2" targetRuntime = "MyBatis3">
<commentGenerator>
<!-- 是否去除自动生成的注释 true :是 :false :否 -->
<property name="suppressAllComments" value="false"/>
</commentGenerator>
<!-- 数据库连接信息 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3307/cloudserver"
userId = "root"
password = ""/>
<!-- 默认为false,将JDBC EDCIMAL和NUMERIC 解析为int,为true时,二者解析为java.math.BigDeciaml -->
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 重点关注以下 -->
<!--javaModelGenerator的 targetProject:生成PO类的位置 -->
<javaModelGenerator targetPackage="com.bao.mybatisTest.domain"
targetProject=".\src">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false"/>
<!-- 从数据库返回值被清除前后的空格 -->
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!--sqlMapGenerator的 targetProject:生成映射文件的位置 -->
<sqlMapGenerator targetPackage="com.bao.mybatisTest.mapper"
targetProject=".\src">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>
<!--javaClientGenerator的 targetProject:生成接口的位置 -->
<javaClientGenerator targetPackage="com.bao.mybatisTest.mapper"
targetProject=".\src"
type = "XMLMAPPER">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false"/>
</javaClientGenerator>
<!-- 指定数据库内表 -->
<table tableName = "test_order"/>
</context>
</generatorConfiguration>
总结:
下载工程或者准备相关环境,修改配置文件,关键修改数据库连接信息,修改生成的POJO类、映射文件、接口文件的路径,以及需要转换的表
十分简单。
(但是我不想进行相关练习,因为目前我不太有必要这么做。如果后续我有必要进行类似的操作时,我会记录当时的结论。)
PageHelper分页插件
开学。
简介:
- 5.1.6版本好用,建议用,几乎支持目前所有的关系型数据库
- PageHelper是一个第三方插件,通过mybatis对外开发的接口进行实现
使用方法
首先添加依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.6</version>
</dependency>
其次配置全局配置文件
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 指定为mysql生成代码 -->
<property name="helperDialect" value="mysql"/>
</plugin>
</plugins>
(当和spring架构整合后,改为配置spring配置文件,本次不提)
最后使用
在查询语句前
加入: PageHelper.startPage(pageNum, pageSize);
演示结构如下:
//4.1添加分页代码
int pageNum = 1; //取第一页
int pageSize = 2; //分页规则,每2个元素就分一页
PageHelper.startPage(pageNum, pageSize);
List<TestOrder> orders= mapper.findAllOrders();
//5. 打印结果
System.out.println(orders.toString());
最终获得结果为:
Page{count=true, pageNum=1, pageSize=2, startRow=0, endRow=2, total=4, pages=2, reasonable=false, pageSizeZero=false}
[TestOrder [order_id=1, user_id=1, buy_info=apple]
, TestOrder [order_id=2, user_id=1, buy_info=iphone]
]
修改页码为2得出
Page{count=true, pageNum=2, pageSize=2, startRow=2, endRow=4, total=4, pages=2, reasonable=false, pageSizeZero=false}
[TestOrder [order_id=3, user_id=2, buy_info=banana]
, TestOrder [order_id=4, user_id=2, buy_info=light]
]
修改页码为3得出
Page{count=true, pageNum=3, pageSize=2, startRow=4, endRow=6, total=4, pages=2, reasonable=false, pageSizeZero=false}[]
以此可以明确分页的规则,将查询的结果根据每页的数量进行分页,并且根据页码取出该页的内容
并且根据打印可以看出,返回的list不再是ArrayList,
此时返回的是PageHelpert提供的Page对象,这个对象有list接口。
关于这个page对象想要获得内部的页码信息,需要进行一次数据封装
PageInfo <TestOrder> ordersPageInfo= new PageInfo <TestOrder> (orders);
之后就可以根据PageInfo类内部的方法获取页码信息
举例:
PageInfo <TestOrder> ordersPageInfo= new PageInfo <TestOrder> (orders);
System.out.println(ordersPageInfo.getPageNum());
打印结果:
1.
结语
以上内容占总课的2%,我从1号学习到了11号,用的是空闲的时间,但是平均每一段都近似花了视频5到10倍的时间,
通常是第一遍,听,第二遍记,第三遍写,然后测试验证,再几遍后,整体过一遍看看有没有记错的地方。
目前基本是掌握了以上的知识点了,继续努力吧。
特殊说明
本文仅做学习用,详细内容请自行购课观看。