一、什么是框架?
框架即一个半成品软件。开发者从头开发一个软件需要花费大量精力,于是有一些项目组开发出半成品软件,开发者在这些软件的基础上进行开发,这样的软件就称之为框架。
如果将开发完成的软件比作是一套已经装修完毕的新房,框架就好比是一套已经修建好的毛坯房。用户直接购买毛坯房,保证建筑质量和户型合理的同时可以进行风格的自由装修。
使用框架开发的好处:
- 省去大量的代码编写、减少开发时间、降低开发难度。
- 限制程序员必须使用框架规范开发,增强代码的规范性,降低程序员之间沟通及日后维护的成本。
- 将程序员的注意力从技术中抽离出来,更集中在业务层面。
使用框架就好比和世界上最优秀的软件工程师共同完成一个项目,并且他们完成的还是基础、全局的工作。
二、什么是ORM框架?
ORM(Object Relationl Mapping),对象关系映射,即在数据库和对象之间作映射处理。
之前我们使用JDBC操作数据库,必须手动进行数据库和对象间的数据转换。
// 新增方法,将对象转为sql语句字段
public void AddUser(User user) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8","root", "root");
String sql = "INSERT INTO user values (null,?,?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1,user.getName());
preparedStatement.setInt(2,user.getAge());
preparedStatement.setString(3,user.getAddress());
preparedStatement.setString(4,user.getSex());
preparedStatement.executeUpdate();
// 省略资源关闭...
}
// 查询方法,将数据库结果集转为对象
public List<User> findAllUser() throws Exception {
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "root");
PreparedStatement preparedStatement = connection.prepareStatement("select * from user");
ResultSet resultSet = preparedStatement.executeQuery();
//遍历查询结果集
List<User> users = new ArrayList<>();
while(resultSet.next()){
// 拿到每一列数据
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
String address = resultSet.getString("address");
String sex = resultSet.getString("sex");
// 将数据封装到对象中
User user = new User();
user.setId(id);
user.setName(name);
user.setAge(age);
user.setAddress(address);
user.setSex(sex);
users.add(user);
}
// 省略资源关闭...
return users;
}
这段代码中,数据库数据与对象数据的转换代码繁琐、无技术含量。而使用ORM框架代替JDBC后,框架可以帮助程序员自动进行转换,只要像平时一样操作对象,ORM框架就会根据映射完成对数据库的操作,极大的增强了开发效率。
三、什么是MyBatis?
MyBatis是一个半自动的ORM框架,其本质是对JDBC的封装。使用MyBatis不需要写JDBC代码,但需要程序员编写SQL语句。之前是apache的一个开源项目iBatis,2010年改名为MyBatis。
补充:
Hibernate也是一款持久层ORM框架,多年前的市场占有率很高,但近年来市场占有率越来越低。
MyBatis与Hibernate的比较:
- MyBatis是一个半自动的ORM框架,需要手写SQL语句。
- Hibernate是一个全自动的ORM框架,不需要手写SQL语句。
- 使用MyBatis的开发量要大于Hibernate。
为什么Hibernate市场占有率越来越低:
- 对于新手学习Hibernate时间成本比MyBatis大很多,MyBatis上手很快。
- Hibernate不需要写SQL语句是因为框架来生成SQL语句。对于复杂查询,开发者很难控制生成的SQL语句,这就导致SQL调优很难进行。
- 之前的项目功能简单,数据量小,所以使用Hibernate可以快速完成开发。而近年来项目的数据量越来越大,而互联网项目对查询速度要求也很高,这就要求我们一定要精细化的调整SQL语句。此时灵活性更强,手动编写SQL语句的MyBatis慢慢代替了Hibernate使用。
- 在高并发、大数据、高性能、高响应的互联网项目中,MyBatis是首选的持久框架。而对于对性能要求不高的比如内部管理系统等可以使用Hibernate。
四、MyBatis入门案例
环境搭建
-
将SQL文件导入数据库
-
创建maven工程,引入依赖
<dependencies> <!-- mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> <!-- mysql驱动包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> <!-- junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> </dependency> <!-- log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> </dependencies>
-
创建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"> <environment id="mysql"> <!-- 事务类型 --> <transactionManager type="JDBC"></transactionManager> <!-- 数据源 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql:///mybatis"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> </configuration>
-
将log4j.properties文件放入resources中,让控制台打印SQL语句。
-
创建实体类
public class User { private int id; private String username; private String sex; private String address; // 省略getter/setter/构造方法/toString方法 }
- 创建持久层接口和映射文件
6.1 在java目录创建持久层接口
public interface UserMapper { List<User> findAll(); }
6.2 在resource目录创建映射文件
<?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.itbaizhan.mapper.UserMapper"> <select id="findAll" resultType="com.itbaizhan.pojo.User"> select * from user </select> </mapper>
6.3 将映射文件配置到mybatis核心配置文件中
<!-- 注册映射文件 --> <mappers> <mapper resource="com/itbaizhan/mapper/UserMapper.xml"> </mapper> </mappers>
映射文件注意事项:
映射文件要和接口名称相同。
映射文件要和接口的目录结构相同。
映射文件中namespace属性要写接口的全名。
映射文件中标签的id属性是接口方法的方法名。
映射文件中标签的resultType属性是接口方法的返回值类型。
映射文件中标签的parameterType属性是接口方法的参数类型。
映射文件中resultType、parameterType属性要写全类名,如果是集合类型,则写其泛型的全类名。
7. 测试持久层接口方法
@Test
public void testFindAll() throws Exception {
// (1)读取核心配置文件
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
// (2)创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// (3)SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory factory = builder.build(is);
// (4)SqlSessionFactory对象获取SqlSession对象
SqlSession session = factory.openSession();
// (5)SqlSession对象获取代理对象
UserMapper userMapper = session.getMapper(UserMapper.class);
// (6)代理对象执行方法
List<User> all = userMapper.findAll();
all.forEach(System.out::println);
// (7)释放资源
session.close();
is.close();
}
五、MyBatis核心对象及工作流程
MyBatis核心对象
SqlSessionFactoryBuilder
SqlSession工厂构建者对象,使用构造者模式创建SqlSession工厂对象。
SqlSessionFactory
SqlSession工厂,使用工厂模式创建SqlSession对象。
SqlSession
该对象可以操作数据库,也可以使用动态代理模式创建持久层接口的代理对象操作数据库。
Mapper
持久层接口的代理对象,他具体实现了持久层接口,用来操作数据库。
MyBatis工作流程
创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder对象构建了SqlSessionFactory对象:构造者模式
SqlSessionFactory对象生产了SqlSession对象:工厂模式
SqlSession对象创建了持久层接口的代理对象:动态代理模式
代理对象操作数据库
六、使用SqlSession操作数据库
除了代理对象能够操作数据库,SqlSession也能操作数据库。只是这种方式在开发中使用的较少,接下来我们使用SqlSession操作数据库:
@Test
public void testFindAll2() throws Exception {
// (1)读取核心配置文件
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
// (2)创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// (3)SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory factory = builder.build(is);
// (4)SqlSessionFactory对象获取SqlSession对象
SqlSession session = factory.openSession();
// (5)SqlSession直接操作数据库
List<User> users = session.selectList("com.itbaizhan.mapper.UserMapper.findAll");
users.forEach(System.out::println);
// (6)关闭资源
session.close();
is.close();
}
七、Mapper动态代理原理
接下来我们通过源码,了解MyBatis的Mapper对象究竟是怎么生成的,他又是如何代理接口的方法。
获取代理对象
点开测试类的getMapper
方法,查看该方法最终调用了什么方法。
当看到Proxy.newProxyInstance
时,可以确定getMapper
方法最终调用的是JDK动态代理方法,且使用MapperProxy类定义代理方式
查看代理方式
点开MapperProxy类,查看invoke方法,查看代理对象是如何工作的。
可以看到,MapperProxy调用了MapperMethod的execute方法定义了代理方式,且底层调用的是SqlSession的方法,根据映射文件标签不同调用不同的SqlSession方法。
结论:
- SqlSession的getMapper方法,最终是调用的是JDK动态代理方法,生成一个代理对象,类型就是传入的接口类型。
- MapperProxy对象通过调用MapperMethod的execute方法定义了代理方式,该方法的底层调用的是SqlSession的方法。
八、MyBatis增删改查
8.1 新增用户
8.1.1 持久层接口添加方法
void add(User user);
8.1.2 映射文件添加标签
<insert id="add" parameterType="com.itbaizhan.pojo.User"> insert into user(username,sex,address) values(#{username},#{sex},#{address}) </insert>
8.1.3 编写测试方法
@Test public void testAdd() throws Exception { InputStream is= Resources.getResourceAsStream("SqlMapConfig.xml"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(is); SqlSession session = factory.openSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = new User("程序员", "男", "上海"); userMapper.add(user); // 提交事务 session.commit(); session.close(); is.close(); }
注意:
- 当接口方法的参数类型为POJO类型时,SQL语句中绑定参数时使用
#{POJO的属性名}
即可。- MyBatis事务默认手动提交,所以在执行完增删改方法后,需要手动调用SqlSession对象的事务提交方法,否则数据库将不发生改变。
8.2 修改用户
优化测试类
我们发现MyBatis的测试方法在操作数据库前都需要获取代理对象,操作数据库后都需要释放资源,可以利用Junit的前置后置方法,优化测试类代码。
InputStream is = null;
SqlSession session = null;
UserMapper userMapper = null;
@Before
public void before() throws IOException {
// (1)读取核心配置文件
is = Resources.getResourceAsStream("SqlMapConfig.xml");
// (2)创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// (3)SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory factory = builder.build(is);
// (4)SqlSessionFactory对象获取SqlSession对象
session = factory.openSession();
// (5)获取代理对象
userMapper = session.getMapper(UserMapper.class);
}
@After
public void after() throws IOException {
// 释放资源
session.close();
is.close();
}
持久层接口添加方法
void update(User user);
映射文件添加标签
<update id="update" parameterType="com.itbaizhan.pojo.User">
update user
set username = #{username},
sex = #{sex},
address=#{address}
where id = #{id}
</update>
编写测试方法
@Test
public void testUpdate(){
User user = new User(8,"程序员1","女","深圳");
userMapper.update(user);
session.commit();
}
8.3 用户删除、根据Id查询
删除用户
持久层接口添加方法
void delete(int userId);
映射文件添加标签
<delete id="delete" parameterType="int">
delete from user where id = #{id}
</delete>
注:当方法的参数类型是简单数据类型时,#{}中可以写任意名称
- 简单数据类型:基本数据类型、字符串等
编写测试方法
@Test
public void testDelete(){
userMapper.delete(8);
session.commit();
}
根据ID查询用户
持久层接口添加方法
User findById(int userId);
映射文件添加标签
<select id="findById" parameterType="int" resultType="com.itbaizhan.pojo.User">
select * from user where id = #{userId}
</select>
编写测试方法
@Test
public void testFindById(){
User user = userMapper.findById(1);
System.out.println(user);
}
8.4 模糊查询
8.4.1 使用#定义参数
持久层接口添加方法
List<User> findByNameLike(String username);
映射文件添加标签
<select id="findByNameLike" parameterType="string" resultType="com.itbaizhan.user.User">
select * from user where username like #{name}
</select>
编写测试方法
@Test
public void testFindByNameLike(){
List<User> users = userMapper.findByNameLike("%王%");
for (User user:users){
System.out.println(user);
}
}
我们看到在映射文件中,parameterType的值为
string
而没有写java.lang.String
,这是为什么呢?
- 参数/返回值类型为基本数据类型/包装类/String等类型时,我们可以写全类名,也可以写别名。
数据类型 别名 byte _byte long _long short _short int _int int _integer double _double float _float boolean _boolean String string Byte byte Long long Short short Integer int/integer Double double Float float Boolean boolean Date date BigDecimal decimal/bigdecimal Object object Map map HashMap hashmap List list ArrayList arraylist Collection collection Iterator iterator
8.4.2 使用$定义参数
模糊查询如果不想在调用方法时参数加%,可以使用拼接参数的方式设置Sql:
<select id="findByUsernameLike" parameterType="string" resultType="com.itbaizhan.pojo.User"> select * from user where username like '%${value}%' </select>
测试方法写法如下:
@Test
public void testFindByNameLike(){
List<User> users = userMapper.findByUsernameLike("程序");
users.forEach(System.out::println);
}
#和$的区别:
- #表示sql模板的占位符,$表示将字符串拼接到sql模板中。
- #可以防止sql注入,一般能用#就不用$。
- ${}内部的参数名必须写value。
8.4.3 使用<bind>定义参数
如果使用#
还不想在调用方法的参数中添加%
,可以使用<bind>
,<bind>
允许我们在 Sql语句以外创建一个变量,并可以将其绑定到当前的Sql语句中。用法如下:
<select id="findByUsernameLike" parameterType="string" resultType="com.itbaizhan.pojo.User">
<bind name="likeName" value="'%'+username+'%'"/>
select * from user where username like #{likeName}
</select>
测试方法写法如下:
@Test
public void testFindByNameLike(){
List<User> users = userMapper.findByUsernameLike("程序");
users.forEach(System.out::println);
}
8.5 分页查询
分页查询时,Sql语句使用limit关键字,需要传入开始索引和每页条数两个参数。MyBatis的多参数处理有以下方式:
顺序传参
Sql中的参数使用arg0,arg1...或param1,param2...表示参数的顺序。此方法可读性较低,在开发中不建议使用。
持久层接口方法
/**
* 分页查询
* @param startIndex 开始索引
* @param pageSize 每页条数
* @return
*/
List<User> findPage(int startIndex,int pageSize);
映射文件
<select id="findPage" resultType="com.itbaizhan.mapper.User">
select * from user limit #{arg0},#{arg1}
</select>
<select id="findPage" resultType="com.itbaizhan.mapper.User">
select * from user limit #{param1},#{param2}
</select>
测试类
@Test
public void testFindPage(){
List<User> users = userMapper.findPage(0,3);
users.forEach(System.out::println);
}
8.6 @Param传参
在接口方法的参数列表中通过@Param定义参数名称,在Sql语句中通过注解中所定义的参数名称指定参数位置。此方式参数比较直观的,推荐使用。
持久层接口方法
List<User> findPage1(@Param("startIndex") int startIndex, @Param("pageSize")int pageSize);
映射文件
<select id="findPage1" resultType="com.itbaizhan.mapper.User">
select * from user limit #{startIndex},#{pageSize}
</select>
测试类
@Test
public void testFindPage1(){
List<User> users = userMapper.findPage1(3,3);
users.forEach(System.out::println);
}
8.7 POJO传参
自定义POJO类,该类的属性就是要传递的参数,在SQL语句中绑定参数时使用POJO的属性名作为参数名即可。此方式推荐使用。
自定义POJO
public class PageQuery {
private int startIndex;
private int pageSize;
// 省略getter/setter/构造方法
}
持久层接口方法
List<User> findPage2(PageQuery pageQuery);
映射文件
<select id="findPage2" resultType="com.itbaizhan.pojo.User" parameterType="com.itbaizhan.pojo.PageQuery">
select * from user limit #{startIndex},#{pageSize}
</select>
测试类
@Test
public void testFindPage2(){
PageQuery pageQuery = new PageQuery(3, 3);
List<User> users = userMapper.findPage2(pageQuery);
users.forEach(System.out::println);
}
8.8 Map传参
如果不想自定义POJO,可以使用Map作为传递参数的载体,在SQL语句中绑定参数时使用Map的Key作为参数名即可。此方法推荐使用。
持久层接口方法
List<User> findPage3(Map<String,Object> params);
映射文件
<select id="findPage3" resultType="com.itbaizhan.pojo.User" parameterType="map">
select * from user limit #{startIndex},#{pageSize}
</select>
测试类
@Test
public void testFindPage3(){
Map<String,Object> params = new HashMap();
params.put("startIndex",0);
params.put("pageSize",4);
List<User> users = userMapper.findPage3(params);
users.forEach(System.out::println);
}
9、聚合查询、主键回填
查询用户总数
持久层接口方法
int findCount();
映射文件
<select id="findCount" resultType="int">
select count(id) from user
</select>
测试类
@Test
public void testFindCount(){
System.out.println(userMapper.findCount());
}
主键回填
有时我们需要获取新插入数据的主键值。如果数据库中主键是自增的,这时我们就需要使用MyBatis的主键回填功能。
持久层接口方法
void add(User user);
映射文件
<insert id="add" parameterType="com.itbaizhan.user.User">
<!-- keyProperty:主键属性名,keyColumn:主键列名,resultType:主键类型,order:执行时机 -->
<selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
SELECT LAST_INSERT_ID();
</selectKey>
insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address})
</insert>
SELECT LAST_INSERT_ID():查询刚刚插入的记录的主键值,只适用于自增主键,且必须和insert语句一起执行。
测试类
@Test
public void testAdd(){
User user = new User("尚学堂", new Date(), "男", "北京");
userMapper.add(user);
session.commit();
System.out.println(user.getId());
}
10、配置文件
10.1 配置文件_<properties>
MyBatis配置文件结构:
-configuration -properties(属性) -property -settings(全局配置参数) -setting -plugins(插件) -plugin -typeAliases(别名) -typeAliase -package -environments(环境) -environment -transactionManager(事务管理) -dataSource(数据源) -mappers(映射器) -mapper -package
properties
属性值定义。properties标签中可以定义属性值,也可以引入外部配置文件。无论是内部定义还是外部引入,都可以使用${name}获取值。
例如:我们可以将数据源配置写到外部的db.properties中,再使用properties标签引入外部配置文件,这样可以做到动态配置数据源。
编写db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=root
在配置文件中引入db.properties
<properties resource="db.properties"></properties>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<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>
当然我们也可以将数据源数据通过
<properties>
配置到MyBatis配置文件内,但这样做没什么意义。
<properties>
<property name="jdbc.driver" value="com.mysql.jdbc.Driver"></property>
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/mybatis"></property>
<property name="jdbc.username" value="root"></property>
<property name="jdbc.password" value="root"></property>
</properties>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<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>
10.2 配置文件_<settings>
<settings>
是配置MyBatis运行时的一些行为的,例如缓存、延迟加载、命名规则等一系列控制性参数。后期我们会使用该标签配置缓存和延迟加载等。
10.3 配置文件_<plugins>
<plugins>
是配置MyBatis插件的。插件可以增强MyBatis功能,比如进行sql增强,打印日志,异常处理等。后期我们会使用该标签配置分页插件。
10.4 配置文件_<typeAliases>
MyBatis对常用类有默认别名支持,比如java.lang.Stirng的别名为string。除此之外,我们也可以使用<typeAliases>
设置自定义别名。
为一个类配置别名
<typeAliases>
<typeAlias type="全类名" alias="别名"></typeAlias>
</typeAliases>
此时我们即可在映射文件中使用自定义别名,如:
配置文件:
<typeAliases>
<typeAlias type="com.itbaizhan.pojo.User" alias="User"> </typeAlias>
</typeAliases>
映射文件
<select id="findAll" resultType="User">
select * from user
</select>
为一个所有包下的所有类配置别名
<typeAliases>
<package name="包名"></package>
</typeAliases>
此时该包下的所有类都有了别名,别名省略包名,和类名相同。如:
配置文件:
<typeAliases>
<package name="com.itbaizhan.pojo"></package>
</typeAliases>
映射文件:
<select id="findPage2" resultType="User" parameterType="PageQuery">
select * from user limit #{startIndex},#{pageSize}
</select>