对着B站动力节点的视频敲的,个人记录一下,感觉入门看这个视频还是够的,但是sql的注解使用,缓存等等都没讲,后续深入学的话可以看刘增辉的《Mybatis从入门到精通》,讲的很好。
一、三层架构
-
界面层:和用户打交道,接收用户的请求参数,显示处理结果(jsp,html,servlet) controller包
-
业务逻辑层:接收了界面层传递的数据,计算逻辑,调用数据库,获取数据。 service包
-
数据访问层:就是访问数据库,执行对数据的查询,修改,删除等等的。 dao包
三层中类的交互:用户使用界面层–>业务逻辑层–>数据访问层–>数据库
对应的处理框架:
- 界面层–servlet–SpringMVC
- 业务逻辑层–service类–Spring
- 数据访问层–dao类–Mybatis
框架是一个半成品的软件,定义好了一些基础功能,需要加入自己的功能就是完整的,基础功能是可重复使用的,可升级的。
框架特点:
- 框架不是全能的,不能做所有的事情
- 框架是针对某一个领域有效,特长在某一个方面,比如Mybatis做数据库操作,做不了其他的。
- 框架是一个软件
JDBC的缺陷
- 代码比较多,开发效率低
- 需要关注Connection,Statement,ResultSet对象创建和销毁
- 对ResultSet查询的结果需要自己封装为List
- 重复的代码太多
- 业务代码和数据库的操作混在一起
二、Mybatis简单使用
Mybatis是Mybatis SQL Mapper Framework for Java (sql映射框架)
- sql mapper:可以把数据库中的一行数据,映射为一个java对象,一行数据可以看做是一个java对象,操作这个对象就相当于操作表中的数据
- Data Access Objects:数据访问,对数据库执行增删改查
Mybatis提供的功能:
-
提供了创建Connection Statement ResultSet的能力,不用自己创建
-
提供了执行sql语句的能力,不用自己执行
-
提供了循环sql,把sql结果转为java对象,封装到List集合
// 也就是之前写JDBC代码时这个操作 while(rs.next()) { Student student = new Student(); student.setID(rs.getInt("id")); student.setName(rs.getString("name")); student.setAge(rs.getInt("age")); studentList.add(student); }
-
提供了关闭资源的能力,不用自己关闭资源对象
开发人员的工作:提供sql语句
一句话总结:Mybatis是一个sql映射框架,提供数据库的操作能力。
使用步骤
1.新建student表
2.加入mybatis坐标,mysql驱动坐标
3.创建实体类,Student
4.创建持久层的dao接口,定义操作数据库的方法
5.创建一个mybatis映射文件:写sql语句的,一般一个表一个sql映射文件,这个文件是xml文件
1)在接口所在的目录中
2)文件名称和接口保持一致
3)映射文件中namespace的值是dao接口的全限定名
4)映射文件中<select><update>等方法的id必须和接口的方法名一致 (遵循这四条后面才能用mybatis的动态代理)
5)dao接口中不要使用重载方法
6.创建mybatis的主配置文件:一个项目就一个主配置文件。主配置文件提供了数据库的链接信息和sql映射文件的位置信息
7.创建使用mybatis类,通过mybatis访问数据库
-
mybatis映射文件:
<?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"> <!--指定约束文件,mybatis-3-mapper.dtd是约束文件的名称,扩展名是dtd的。约束文件是用来限制和检查在当前文件中出现的标签,属性必须符合mybatis的要求。--> <mapper namespace="cn.youkee.dao.StudentDao"> <!-- select:表示查询操作 id:你要执行的sql语法的唯一标识,mybatis会使用这个id的值来找到要执行的sql语句 可以自定义,但是要求使用接口中的方法名称 returnType:表示结果类型的,是sql语句执行后得到的ResultSet,遍历这个ResultSet得到的java对象的类型 值写的是类的全限定名称 --> <select id="selectStudents" resultType="cn.youkee.domain.Student"> select id, name, email, age from student order by id </select> </mapper> <!--mapper是当前文件的根标签,必须的 namespace:叫做命名空间,唯一值,可以是自定义的字符串,要求你使用dao接口的全限定名称 在当前文件中,可以使用特定的标签,表示数据库的特定操作 <select>表示查询 <update>表示更新数据库的操作 <insert>表示插入 <delete>表示删除...-->
-
mybatis主配置文件:(后续学框架很多会去掉,不会这么复杂)
<?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> <!--指定properties文件的位置,从类路径根(maven编译后的target/classes)开始找文件,这里将jdbc.properties放在了resources下,并mark directory as Resource,这样maven编译的时候会直接放在类路径下--> <properties resource="jdbc.properties" /> <!--settings:控制mybatis的全局行为--> <settings> <!--输出日志到控制台--> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <!--环境配置:数据库的链接信息 default:必须和某个environment的id值一样,告诉mybatis使用哪个数据库的连接 --> <environments default="development"> <!--environment:一个数据库信息的配置id:一个唯一值,自定义,表示环境的名称--> <environment id="development"> <!--TransactionManager:mybatis的事务类型,Spring里提过,mybatis和JDBC用的一样都是 DataSourceTansactionManager type: JDBC(表示使用JDBC中Connection对象的commit,rollback做事务处理)--> <transactionManager type="JDBC"/> <!--datasource:表示数据源,连接数据库的 type:表示数据源的类型,POOLED表示使用连接池--> <dataSource type="POOLED"> <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> </environment> </environments> <!--sql mapper(sql映射文件)的位置--> <mappers> <!--一个mapper标签指定一个文件的位置 从类路径开始的路径信息 --> <mapper resource="cn/youkee/dao/StudentDao.xml"/> <!--或者可以用package,这个包下的所有xml都会被扫描,但是这个xml文件的名称必须和接口的名称一样(区分大小写),而且在同一个目录之中--> <package name="cn.youkee.dao" /> </mappers> </configuration> <!-- mybatis的主配置文件:主要定义了数据库的配置信息,sql映射文件的位置 1.约束文件: <?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"> 2.configuration 根标签 -->
使用mybatis查询数据(很麻烦,后续会简化的)
public class MyApp {
public static void main(String[] args) throws IOException {
// 访问mybatis读取student数据
// 1.定义mybatis主配置文件的名称,从类路径的根开始(target/classes)
String config = "mybatis.xml";
// 2.读取这个config表示的文件
InputStream in = Resources.getResourceAsStream(config);
// 3.创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 4.创建SqlSessionFactory对象
SqlSessionFactory factory = builder.build(in);
// 5.[重要]获取SqlSession对象,从SqlSessionFactory中获取SqlSession
SqlSession sqlSession = factory.openSession();
// 6.[重要]指定要执行的sql语句的标识 sql映射文件中的namespace + "." + 标签的id值
// 注意如果执行insert,update,delete操作,mybatis默认是不提交事务的
String sqlID = "cn.youkee.dao.StudentDao" + "." + "selectStudents";
// sqlSession.commit();
// 7.执行sql语句,通过sqlID找到语句
List<Student> studentList = sqlSession.selectList(sqlID);
// 8.输出结果
studentList.forEach(student -> System.out.println(student));
// 9.关闭SqlSession对象
sqlSession.close();
}
}
mybatis默认会把JDBC的自动提交事务设为false:Setting autocommit to false on JDBC Connection;最后在关闭sqlSession的时候会再设为true
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2d9d4f9d]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2d9d4f9d]
Returned connection 765284253 to pool.
Mybatis主要类的介绍
-
Resources:Mybatis中的一个类,负责读取主配置文件
String config = "mybatis.xml"; InputStream in = Resources.getResourceAsStream(config);
-
SqlSessionFactoryBuilder:创建SqlSessionFactory对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(in);
-
SqlSessionFactory:重量级对象,程序创建一个对象耗时比较长,使用资源比较多。整个项目中有一个就够了。本身是个接口,接口的实现类是DefaultSqlSessionFactory。主要是为了获得SqlSession对象。
SqlSession sqlSession = factory.openSession();
openSession()方法说明:
- openSession()无参的,获取非自动提交事务的SqlSession对象
- openSession(true),获取自动提交事务的对象,false就非自动提交的
-
SqlSession:接口,定义了操作数据的方法,例如:selectOne(), selectList(), insert(), commit(), rollback()。实现类是DefaultSqlSession。
使用要求:SqlSession对象不是线程安全的,需要在方法内部使用,在执行sql语句之前,使用openSession()获取SqlSession对象,在执行完sql语句自后,需要关闭它,执行SqlSession.clos()。这样能保证他的使用时线程安全的。
三、Mybatis核心功能
使用Mybatis的动态代理
上述只有SqlSession是每次数据库操作都需要的,可以写个工具类获取,然后用完后自己关了。
public class MybatisUtils {
private static SqlSessionFactory factory;
static {
String config = "mybatis.xml";
try {
InputStream in = Resources.getResourceAsStream(config);
factory = new SqlSessionFactoryBuilder().build(in);
} catch (IOException e) {
e.printStackTrace();
}
}
// 获取SqlSession的方法
public static SqlSession getSqlSession() {
SqlSession sqlSession = null;
if (factory != null) {
sqlSession = factory.openSession();
}
return sqlSession;
}
}
那么这个时候的代码就可以改为:
public interface StudentDao {
List<Student> selectStudents();
int insertStudent(Student student);
}
public class StudentDaoImpl implements StudentDao {
@Override
public int insertStudent(Student student) {
SqlSession sqlSession = MybatisUtils.getSqlSession();
String sqlID = "cn.youkee.dao.StudentDao.insertStudent";
int ret = sqlSession.insert(sqlID, student);
sqlSession.commit();
sqlSession.close();
return ret;
}
@Override
public List<Student> selectStudents() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
String sqlID = "cn.youkee.dao.StudentDao.selectStudents";
List<Student> students = sqlSession.selectList(sqlID);
sqlSession.close();
return students;
}
}
这个时候,假如我们需要在业务层调用某个DAO方法,那么代码就是这样的:
public void queryService() {
/*
List<Student> studentList = studentDao.selectStudents();
这行代码中:
1.dao对象的类型是StudentDao,全限定名是cn.youkee.dao.StudentDao,这里和上面配置的namespace是一样的
2.方法selectStudents()和mapper文件中的id值selectStudents是一样的
3.等于说只要调用这个方法,就可以自动把上面的sqlID拼出来,而且根据mapper的配置,还可以知道到底执行的是select操作还 是insert操作,还可以根据dao方法的返回值确定是不是selectList。
mybatis动态代理:mybatis根据dao的方法调用,获取执行sql语句的信息。mybatis根据你的dao接口,创建出一个dao
的实现类,并创建这个类的对象,完成SqlSession调用方法,访问数据库。也就是说自己连DAOImpl都不用写了。
用的是jdk的动态代理,也就是需要接口的。
*/
StudentDao studentDao = new StudentDaoImpl();
List<Student> studentList = studentDao.selectStudents();
studentList.forEach(System.out::println);
}
public void TestSelectStudents() {
/*
使用Mybatis的动态代理机制,使用SqlSession.getMapper(dao接口)
getMapper能获取dao接口对应的实现类对象
*/
SqlSession sqlSession = MybatisUtils.getSqlSession();
StudentDao dao = sqlSession.getMapper(StudentDao.class);
List<Student> studentList = dao.selectStudents();
studentList.forEach(System.out::println);
sqlSession.close();
}
传入参数
从java代码中把数据传入到mapper文件的sql语句中(比如where中的条件)
-
parameterType:mapper文件中的一个属性,表示dao接口中方法的参数的数据类型,例如StudentDao接口中有一个根据Id获取学生的方法
// 一个简单类型的参数。mybatis把基本类型数据和String都叫简单类型 public Student selectStudentById(Integer id);
那么需要在mapper文件中配置
```xml
<select id="selectStudentById" resultType="cn.youkee.domain.Student" parameterType="java.lang.Integer">
select id, name, email, age from student where id = #{id}
</select>
<!--
parameterType:dao接口中方法参数的数据类型,它的值是java数据类型全限定名称或者mybatis定义的别名
注意:parameterType不是强制的,mybatis通过反射机制能够发现接口参数的类型,所以可以没有
#{}只是占位符,括号里的内容是任意的,方便区分就可以。mybatis执行的是jdbc中的PreparedStatement对象
由mybatis执行下面的代码:
1.mybatis创建Connection,PreparedStatement
String sql = “select id, name, email, age from student where id = ?”
PreparedStatement pst = conn.prepareStetement(sql);
pst.setInt(1, 1001);
2.执行sql封装为resultType="cn.youkee.domain.Student"这个对象
ResultSet rs = rs.executeQuery();
while(rs.next()) {
Student student = new Student();
student.setId(re.getInt("Id"));
...
}
return student;// 赋給了dao方法调用的返回值
-->
然后用的时候直接调方法就行了。
下面是mybatis定义的别名,这样就不用写全限定名。
-
@Param命名参数,传多个参数
比如:
public List<Student> selectMutiParam(@Param("myName") String name, @Param("myAge") Integer age);
那么mapper文件中就需要这么配置:
<select id="selectMultiParam" resultType="cn.youkee.domain.Student"> select * from student where name=#{myName} or age=#{myAge} </select>
-
传入一个对象
定义一个cn.youkee.vo.QueryParam.java对象
public class QueryParam { private String paramName; private Integer paramAge; ... }
然后再StudentDao中定义一个方法
List<Student> selectMultiObject(QueryParam param);
mapper中就需要这么定义
<!-- 使用对象的语法:#{属性名,javaType=类型名称,jdbcType=数据类型},这是完整的语法,很少用 javaType:值java中的属性数据类型,jdbcType:在数据库中的数据类型 例如:#{paraName, javaType=java.lang.String, jdbcType=VARCHAR} 使用简化的就可以,只传属性名就ok,javaType和jdbcType会自动获取 --> <select id="selectMultiParam" resultType="cn.youkee.domain.Student"> select * from student where name=#{paramName} or age=#{paramAge} </select>
其实未必要单独写一个QueryParam对象,用之前的Student对象就可以。只需要把这个Student对象对应的属性设置为你想查询的值就可以,别的属性的值(比如这个例子里的Id和email)可以不设置,就不单独写了。
-
按位置传递参数,这个了解就可以,不常用
比如还是上面的例子需要传name和age,这时mapper文件里的占位符用#{arg0}和#{arg1}分别对应这两个参数。(mybatis 3.4之前是#{0}和#{1})。
-
使用Map传递参数,这个也不常用
Map<String, Object> map = new HashMap<>(); map.put("myName", "张三"); map.put("myAge", 20);
那么mapper文件中的占位符还是用#{myName}和#{myAge}就可以了。接口中的方法参数也是Map,**可读性太差了,单看接口啥都看不出来。**阿里巴巴开发手册也禁止用map当参数。
-
#和$的区别
#用的是占位符,对应PreparedStatement,所以#{id}执行的sql语句是
select * from student where id = ?;
$用的是sql拼接,对应Statement对象,有sql注入的风险,并且执行的效率也低,执行${id}的结果是
select * from student where id = 1001; // 1001是查询的时候传的参数
但是$ 可以用来替换列名或表名,比如oder by ${colName}
Mybatis的输出结果
mybatis执行了sql语句,得到java对象。
-
resultType:结果类型,指sql语句执行完毕后,数据转为的java对象。之前的select标签里就配过,是方法结果得到对象的全限定名(或者上面表里的别名)。
这个对象可以是java基础类,也可以是数据库表对应的实体类,也可以是根据查询需要定义的新的类(比如多表查询的结果)。
处理方式:
- mybatis执行sql语句,然后调用类的无参数构造方法,创建对象
- mybatis把ResultSet指定列值赋给同名的属性 (jdbc对应的做法上面写过了就不写了)
1.实体对象也可以定义别名,需要在mybatis.xml中配置typeAlias,然后resultType就可以用别名stu(不建议)
2.或者把domain包给配进去,这样写resultType的时候就不用写前面的包名,直接写Student就可以(也不是很建议,如果多个包下的类名相同,就没办法区分了)
最建议的还是用全限定名
<typeAliases> 1.<typeAlias type="cn.youkee.domain.Student" alias="stu" /> 2.<package name="cn.youkee.domain" /> </typeAliases>
resultType还可以是Map的,key对应列名,value对应列值,但是因为key不可重复,所以只能返回一行数据,没啥实际意义。
-
resultMap:结果映射,指定列名和java对象的属性对应关系,上面讲的都是列名和属性名一样,然后mybatis直接赋值。
但是当列名和属性名不一样时,一定要用resultMap,定义列值赋值给哪个属性。万一数据库的列名和自己定义的属性名不一样,这个就派上用场了(在实际开发中很常见)。在映射文件中应该这么配置:
<!--现在假设数据库的列名和java对象的属性名都不一样。id是自定义名称,用来引用这个resultMap,type是java类型的全限定名--> <resultlMap id="studentMap" type="cn.youkee.domain.Student"> <!--主键列,使用id。column对应数据库表的列名,property对应java对象的属性名--> <id column="id" property="StuId" /> <!--非主键列,使用result--> <result column="name" property="StuName" /> <result column="email" property="StuEmail" /> <result column="age" property="StuAge" /> </resultlMap> <select id="selectAllStudents" resultMap="studentMap"> select id, name, email, age from student </select>
resultType和resultMap二者不能同时用!!!resultMap里已经指定了类的全限定名了
还有一种解决办法就是select的时候把列名都给起个别名,比如select id as StuId, name as StuName… 这样不配resultMap也可以,用resultType就可以一一对应了。因为resultType的默认原则就是同名列值赋给同名属性。
模糊查询
-
第一种方法,java代码指定like的内容
<select id="selectLikeOne" resultType="cn.youkee.domain.Student"> select id, name, email, age from student where name like #{namae} </select>
List<Student> list = dao.selectLikeOne("%李%");
-
第二种方式,在mapper文件中拼接。这个时候java传的就是"李",需要在mapper中拼 %,mapper中name前后的空格都必须有。而且这个时候如果只需要一个%的话就需要改mapper文件,不灵活。
<select id="selectLikeTwo" resultType="cn.youkee.domain.Student"> select id, name, email, age from student where name like "%" #{namae} "%" </select>
List<Student> list = dao.selectLikeOne("李");
四、动态SQL
动态sql:sql语句是变化的,可以根据条件获取到不同的sql语句。主要是where部分发生变化。
动态sql的实现,使用的是mybatis提供的标签<if> <where><foreach>。动态sql要使用java对象作为参数。
<if>
<if>是判断条件的,语法是:
<if test="判断java对象的属性值">
sql
</if>
<select id="selectStudentIf" resultType="cn.youkee.domain.Student">
select * from student
where
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="age > 0">
or age > #{age}
</if>
</select>
List<Student> selectStudentIf(Student student);
但是如果name的test不通过,age的test通过,这个时候就会多出来or,会报语法错误,所以单独使用意义不是很大。需要结合别的标签一块用。
<where>
<where>用来包含多个<if>,当多个if有一个成立的,<where>会自动增加一个where关键字,并去掉if中多余的and 、or等,写法如下:
<select id="selectStudentWhere" resultType="cn.youkee.domain.Student">
select * from student
<where>
<if test="name != null">
name = #{name}
</if>
<if test="age > 0">
or age > #{age}
</if>
</where>
</select>
<foreach>
<foreach>循环java中的数组,List集合的,主要是用在sql的in语句,比如:
select * from student where id in (1001, 1002, 1003);
foreach的语法如下:
<foreach collection="" item="" open="" close="" separator=""></foreach>
<!--
collection:表示接口中的方法参数的类型,如果是数组使用array,如果是list集合使用list
item:自定义的,表示数组或集合成员的变量
open:循环开始时的字符
close:循环结束时的字符
separator:集合成员之间的分隔符
-->
-
简单类型的List
具体配置应该这么配:
List<Student> selectForeachOne(List<Integer> idList);
<select id="selectForeachOne" resultType="cn.youkee.domain.Student"> select * from student where id in <foreach collection="list" item="myid" open="(" close=")" separator=","> #{myid} </foreach> </select>
测试方法如下:
@Test public void TestForeachOne() { SqlSession sqlSession = MybatisUtils.getSqlSession(); StudentDao dao = sqlSession.getMapper(StudentDao.class); List<Integer> list = new ArrayList<>(Arrays.asList(1001, 1002, 1003)); List<Student> studentList = dao.selectForeachOne(list); studentList.forEach(System.out::println); sqlSession.close(); }
通过控制台日志,可以看到执行的sql语句为:
Preparing: select * from student where id in ( ? , ? , ? )
Parameters: 1001(Integer), 1002(Integer), 1003(Integer)
Columns: id, name, email, age
Row: 1001, 李四, lisi@qq.com, 20
Row: 1002, 张三, zhangsan@gmail.com, 21
Row: 1003, 张飞, zhangfei@foxmail.com, 26
Total: 3 -
自定义对象的List
List<Student> selectForeachTwo(List<Student> stuList);
<select id="selectForeachTwo" resultType="cn.youkee.domain.Student"> select * from student where id in <foreach collection="list" item="stu" open="(" close=")" separator=","> #{stu.id} 相当于调用stu.getId() </foreach> </select>
代码片段
复用一些sql语句
<sql id="selectStudent">
select * from student
</sql>
<select id="..." resultType="...">
<include refid="selectStudent"/> where
...
</select>
五、PageHelper
PageHelper用来做数据分页的,非常重要,后面用数据库离不开分页!
maven引入依赖之后,需要在mybatis主配置文件<environment>标签之前加上插件的配置
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="reasonable" value="true"/>
</plugin>
</plugins>
代码中用起来非常方便
@Test
public void TestSelectAllStudents() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
StudentDao dao = sqlSession.getMapper(StudentDao.class);
// 第一个参数:pageNum,第几页,从1开始
// 第二个参数:pageSize,一页中几行数据
PageHelper.startPage(2, 2);
List<Student> studentList = dao.selectAllStudents();
studentList.forEach(System.out::println);
sqlSession.close();
}
通过看控制台日志,会发现他先会查询数据库,看一共有多少行数据:
Preparing: SELECT count(0) FROM student
Parameters:
Columns: count(0)
Row: 5
Total: 1
然后再执行查询的sql语句:
Preparing: select * from student order by id LIMIT ?, ?
Parameters: 2(Integer), 2(Integer)
Columns: id, name, email, age
Row: 1003, 张飞, zhangfei@foxmail.com, 26
Row: 1004, 刘备, liubei@163.com, 22
Total: 2
这里规则就和sql的一样了,从第2条数据开始,取2条。相当于不用自己去计算从第几条开始了,只需要指定页数就行了。