深入:MyBatis详解 - 总体框架设计 | Java 全栈知识体系 (pdai.tech)
什么是MyBatis?
- MyBatis是一款优秀的持久层框架
- MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程,减少了代码的冗余,减少程序员的操作。
- MyBatis 可以使用简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO (Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录
JDBC
public class JDBCDemo {
public static void main(String[] args) {
String jdbcUrl = "jdbc:mysql://localhost:3306/testdb";
String username = "root";
String password = "password";
try {
// 加载JDBC驱动程序
Class.forName("com.mysql.cj.jdbc.Driver");
// 建立数据库连接
Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
// 创建Statement对象
Statement statement = connection.createStatement();
// 执行查询语句
ResultSet resultSet = statement.executeQuery("SELECT * FROM users");
// 处理结果集
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
System.out.println("ID: " + id + ", Name: " + name);
}
// 使用PreparedStatement执行带参数的查询
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, 1);
ResultSet preparedStatementResultSet = preparedStatement.executeQuery();
// 处理结果集
if (preparedStatementResultSet.next()) {
int id = preparedStatementResultSet.getInt("id");
String name = preparedStatementResultSet.getString("name");
System.out.println("ID: " + id + ", Name: " + name);
}
// 关闭资源
resultSet.close();
preparedStatementResultSet.close();
statement.close();
preparedStatement.close();
connection.close();
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
}
}
使用 JDBC 访问数据库通常分为四个主要步骤。这些步骤适用于大多数的 JDBC 操作,无论是查询、插入、更新还是删除数据。
1. 加载数据库驱动程序
第一步是加载数据库的 JDBC 驱动程序。驱动程序是数据库与 Java 之间的桥梁,它使得 Java 能够与特定类型的数据库进行通信。
Class.forName("com.mysql.cj.jdbc.Driver");
对于现代的 JDBC,尤其是在使用 JDBC 4.0 及以后的版本时,这一步是可选的,因为 JDBC 会自动检测并加载驱动程序。
2. 创建数据库连接
第二步是使用 DriverManager
获取一个 Connection
对象,该对象表示与数据库的连接。你需要提供数据库的 URL、用户名和密码。
String url = "jdbc:mysql://localhost:3306/your_database";
String username = "your_username";
String password = "your_password";
Connection connection = DriverManager.getConnection(url, username, password);
3. 创建并执行 SQL 语句
接下来,需要创建一个 Statement
或 PreparedStatement
对象,并通过它执行 SQL 语句。你可以执行查询(SELECT
)或更新(INSERT
、UPDATE
、DELETE
)操作。
String sql = "SELECT * FROM users";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
4. 处理结果集和关闭资源
最后一步是处理查询的结果,并关闭所有的 JDBC 资源,包括 ResultSet
、Statement
和 Connection
。为了避免资源泄漏,建议在完成操作后及时关闭 ResultSet
、Statement
和 Connection
。使用 try-with-resources
语句可以简化资源管理:
try (Connection connection = DriverManager.getConnection(url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
System.out.println("ID: " + id + ", Username: " + username);
}
} catch (SQLException e) {
e.printStackTrace();
}
1. Driver
- Driver 接口:
- 负责建立与数据库的连接。
- 各个数据库厂商提供的 JDBC 驱动程序实现了此接口。
- 不直接使用,而是通过
DriverManager
管理和使用。
2. DriverManager
- DriverManager 类:
- 用于管理一组 JDBC 驱动程序。
- 负责加载 JDBC 驱动程序和建立数据库连接。
- 常用方法:
getConnection(String url, String user, String password)
:通过指定的 URL、用户名和密码获取数据库连接。getConnection(String url)
:通过指定的 URL 获取数据库连接。
3. Connection
- Connection 接口:
- 表示与特定数据库的连接会话。
- 负责创建
Statement
对象、管理事务和关闭连接。 - 常用方法:
createStatement()
:创建一个用于执行 SQL 语句的Statement
对象。prepareStatement(String sql)
:创建一个用于执行预编译 SQL 语句的PreparedStatement
对象。setAutoCommit(boolean autoCommit)
:设置事务是否自动提交。commit()
:提交事务。rollback()
:回滚事务。close()
:关闭连接。
4. Statement
- Statement 接口:
- 用于执行静态 SQL 语句并返回结果。
- 常用方法:
executeQuery(String sql)
:执行查询语句并返回结果集。executeUpdate(String sql)
:执行更新语句(如INSERT
、UPDATE
、DELETE
)并返回受影响的行数。execute(String sql)
:可以执行查询、更新或其他类型的 SQL 语句。close()
:关闭Statement
对象。
5. PreparedStatement
- PreparedStatement 接口:
- 继承自
Statement
接口,用于执行预编译的 SQL 语句。 - 通过参数化查询,可以有效防止 SQL 注入。
- 常用方法:
setInt(int parameterIndex, int x)
:设置整数参数。setString(int parameterIndex, String x)
:设置字符串参数。setDate(int parameterIndex, Date x)
:设置日期参数。executeQuery()
:执行查询语句并返回结果集。executeUpdate()
:执行更新语句并返回受影响的行数。close()
:关闭PreparedStatement
对象。-
prepareStatement对传递的参数中的敏感字符进行转义从而防止SQL注入
- 执行存储过程的对象
CallableStatement prepareCall (sql)
- 继承自
6. ResultSet
- ResultSet 接口:
- 表示查询结果集的数据表。
- 提供读取和操作结果集的方法。
- 常用方法:
next()
:将光标移到下一行。getInt(String columnLabel)
:获取指定列的整数值。getString(String columnLabel)
:获取指定列的字符串值。getDate(String columnLabel)
:获取指定列的日期值。close()
:关闭ResultSet
对象。
7. DataSource
- DataSource 接口:
- 提供一种标准的方式来获取数据库连接。
- 一般用于连接池实现。
- 通过 JNDI(Java Naming and Directory Interface)来查找和获取
DataSource
对象。 - 常用方法:
getConnection()
:获取数据库连接。
8. SQLException
- SQLException 类:
- 表示 JDBC 操作中发生的错误或异常。
- 提供了获取错误信息和错误码的方法。
- 常用方法:
getMessage()
:获取错误消息。getErrorCode()
:获取错误码。getSQLState()
:获取 SQL 状态码。
事务管理
数据库连接池
- 据库连接池是个容器,负责分配、管理数据库连接(Connection)
- 它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个
- 释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏
优势:
- 资源重用
- 提升系统响应速度
- 避免数据库连接遗漏
Lombok
PageHelper
PageHelper
是一个用于在 MyBatis 中进行分页查询的开源分页插件。它可以帮助简化分页查询的代码,提供了一些方便的方法来进行分页处理。
pagehelper的依赖:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> </dependency>
使用思路:
表现层:
- 接收参数(分页参数)
- 调用service进行分页条件查询获取PageBean
- 响应
业务层:
- 使用PageHelper完成分页条件查询
- 封装PageBean对象,返回
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}
在controller中用DTO接收分页参数,DTO中包含pageNum页码,pageSize每页大小这两个属性 ,PageResult包含Long total总记录数和List records当前页数据集合。
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> list = page.getResult();
return new PageResult(total, list);
在server的实现类中
- 使用 PageHelper.startPage 方法启动分页,pageNum 表示页码,pageSize 表示每页大小 PageHelper.startPage(pageNum, pageSize);
- 执行查询操作,查询结果会被封装到 PageInfo 对象中,Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);此时完整应该是List<Employee> empList=employeeMapper..Page<Employee> page=(Page<Employee>)empList;
- 获取 PageInfo 对象,其中包含了分页信息,List<Employee> list = page.getResult();
- 返回查询结果
PageInfo
对象是 MyBatis 分页插件 PageHelper 返回的分页信息对象。它包含了分页查询的相关信息,帮助开发者了解当前分页的状态、总记录数、总页数等信息。
PageInfo
类主要提供了以下一些常用的方法和属性:
-
List<T> getList()
: 获取当前页的数据列表。List<T> getResult() -
int getPageNum()
: 获取当前页码。 -
int getPageSize()
: 获取每页记录数。 -
int getSize()
: 获取当前页的记录数。 -
long getTotal()
: 获取总记录数。 -
int getPages()
: 获取总页数。
基础操作
参数占位符
删除
增加(主键返回)
更新
查询
XML映射文件
在实际项目中发现不同包名的情况下也可以
使用注解来映射简单语向会码显得更加简洁,但对于稍周复杂一点的,JAVA注解不仅力不以心,还会让你本就复杂的 SQL语更加混乱不堪,一你猜如果你需要做一些很复杂的操作,最好用XML 来映射语句。
动态SQL
<if><where><set><trim>
<trim>
用于处理 SQL 片段的前后空白字符和特定的 SQL 关键字,通常用于动态生成 SQL 语句时清理多余的关键字或符号。
<select id="selectUsers" parameterType="map" resultType="User">
SELECT * FROM users
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</trim>
</select>
-
<trim>
标签:可以用于处理 SQL 片段的前缀和后缀,代替<where>
和<set>
标签,以实现动态生成 SQL 的功能。prefix
属性:在结果前添加特定的 SQL 关键字(如WHERE
,SET
)。suffixOverrides
属性:去除结果后的多余关键字(如,
)。prefixOverrides
属性:去除结果前的多余关键字(如AND
,OR
)。
-
<where>
标签:自动添加WHERE
子句,并处理多余的前缀(AND
,OR
),适用于条件查询。 -
<set>
标签:自动添加SET
子句,并去除多余的逗号,适用于更新操作。
<select>
<insert>
<update>
<delete>
这些标签用于定义基本的 CRUD 操作,即查询、插入、更新和删除。
<select id="selectUser" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<insert id="insertUser" parameterType="User">
INSERT INTO users(name, email) VALUES (#{name}, #{email})
</insert>
<update id="updateUser" parameterType="User">
UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}
</update>
<delete id="deleteUser" parameterType="int">
DELETE FROM users WHERE id = #{id}
</delete>
<resultMap>
用于定义结果映射,将查询结果映射到 Java 对象中。
通过 resultType
属性,你可以直接指定结果集映射到一个 Java 类。如果 SQL 查询结果列名与 Java 类的属性名一致,MyBatis 能够自动完成基本的映射。
在这个例子中,resultType="User"
表示查询结果将自动映射到 User
类的实例。如果表列名与 User
类的属性名一致,这种映射方式就能正常工作。
<select id="selectUser" parameterType="int" resultType="User">
SELECT id, name, email FROM users WHERE id = #{id}
</select>
使用 <resultMap>
可以提供更精细的控制,特别是在列名与属性名不一致,或者需要对复杂的对象进行映射时。
<resultMap id="visitMap" type="com.std.customer.domain.entity.StdVisit">
<result column="visit_id" property="visitId"/>
<result column="salesman_id" property="salesmanId"/>
<collection property="visitMediaList" javaType="java.util.List" ofType="com.std.customer.domain.entity.StdVisitMedia">
<result column="visit_media_id" property="visitMediaId"/>
<result column="m_object_version_number" property="objectVersionNumber"/>
</collection>
</resultMap>
-
<resultMap>
:id="visitMap"
:为这个映射定义一个唯一的标识符,可以在查询中引用。type="com.std.customer.domain.entity.StdVisit"
:指定这个映射的目标 Java 对象类型,即StdVisit
类。
-
<result>
:- 用于定义数据库列到 Java 对象属性的映射。
column="visit_id"
对应于property="visitId"
:表示 SQL 查询中的visit_id
列会映射到StdVisit
类的visitId
属性。
-
<collection>
:- 用于定义一对多的关系,表示一个对象中的一个属性是一个集合。
property="visitMediaList"
:表示StdVisit
类中的visitMediaList
属性是一个List
类型的集合。javaType="java.util.List"
:指定集合的 Java 类型。ofType="com.std.customer.domain.entity.StdVisitMedia"
:指定集合中元素的类型,即StdVisitMedia
类。
这个 resultMap
配置适用于以下场景:
-
一对多关系:
- 当一个对象(如
StdVisit
)具有一个集合属性(如visitMediaList
),并且这个集合需要从查询结果中填充时,可以使用<collection>
标签进行映射。
- 当一个对象(如
-
复杂映射需求:
- 当查询结果集包含多层嵌套数据时,
<resultMap>
提供了灵活的映射方式,以处理这种复杂的数据结构。
- 当查询结果集包含多层嵌套数据时,
<association><collection>
<resultMap id="orderMap" type="com.example.Order">
<id column="order_id" property="orderId"/>
<result column="order_date" property="orderDate"/>
//假设有一个 Order 类,其中包含一个 User 对象。
<association property="user" javaType="com.example.User">
<id column="user_id" property="userId"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
</association>
//假设有一个 Order 类,其中包含一个 List<Product> 对象。
<collection property="productList" ofType="com.example.Product">
<id column="product_id" property="productId"/>
<result column="product_name" property="productName"/>
<result column="price" property="price"/>
</collection>
</resultMap>
<association>
: 映射单个对象的嵌套属性,用于处理一对一或多对一的关系。<collection>
: 映射集合属性,用于处理一对多或多对多的关系。
<foreach>
- collection: 集合名称
- item:集合遍历出来的元素/项
- separator:每一次遍历使用的分隔符
- open: 遍历开始前拼接的片段
- close: 遍历结束后拼接的片段
<sql><include>
<include>用于引入其他 SQL 片段,使得 SQL 代码可以复用。
<sql>用于定义可复用的 SQL 片段,可以通过 <include>
引用。
<sql id="userColumns">
id, name, email
</sql>
<select id="findAllUsers" resultType="User">
SELECT <include refid="userColumns"/> FROM users
</select>
<choose>
, <when>
, <otherwise>
-
<choose>
:- 用于封装一组条件判断。它是一个容器标签,包含一个或多个
<when>
标签和一个可选的<otherwise>
标签。<choose>
标签帮助你定义复杂的条件逻辑。
- 用于封装一组条件判断。它是一个容器标签,包含一个或多个
-
<when>
:- 用于定义条件。它通常与
<choose>
一起使用,表示当满足特定条件时的 SQL 片段。 test
属性指定条件表达式,符合条件的 SQL 片段会被包含在最终的 SQL 语句中。
- 用于定义条件。它通常与
-
<otherwise>
:- 用于定义默认的 SQL 片段。当没有
<when>
标签的条件满足时,<otherwise>
标签中的 SQL 片段将被包含在最终的 SQL 语句中。 - 如果没有
<otherwise>
标签,且没有任何<when>
标签的条件满足,则不会添加任何 SQL 片段。
- 用于定义默认的 SQL 片段。当没有
<select id="selectUsers" parameterType="map" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="name != null">
AND name = #{name}
</when>
<when test="email != null">
AND email = #{email}
</when>
<otherwise>
AND active = 1
</otherwise>
</choose>
</where>
</select>
- 如果
name
参数不为空,则 SQL 查询包含AND name = #{name}
。 - 如果
name
参数为空但email
参数不为空,则 SQL 查询包含AND email = #{email}
。 - 如果两个参数都为空,则 SQL 查询包含
AND active = 1
。
<choose>
的优势
-
互斥条件:
<choose>
和<when>
更适合处理互斥条件的场景。当你希望只根据第一个满足条件的<when>
来决定执行的 SQL 片段时,<choose>
就显得更有必要。多个<if>
标签是并列的,它们会逐个检查并且可能都执行,而<when>
则是互斥的,只会执行第一个满足条件的语句。 -
默认行为:通过
<otherwise>
,你可以轻松定义默认行为,即当所有条件都不满足时,系统应该如何处理。多个<if>
标签虽然可以实现类似的逻辑,但不如<otherwise>
直观和简洁。
<bind>
用于创建一个新的变量,并在 SQL 语句中使用。
<select id="findUserByKeyword" parameterType="String" resultType="User">
<bind name="keyword" value="'%' + keyword + '%'" />
SELECT * FROM users WHERE name LIKE #{keyword}
</select>
<if test="signTimeEnd != null">
and sv.sign_time <![CDATA[ < ]]> #{signTimeEnd}
</if>
<![CDATA[ < ]]>
: <![CDATA[ ... ]]>
是 XML 的一个特殊语法,用于包裹不需要被解析的内容。通常在 SQL 语句中使用比较运算符(如 <
, >
, <=
, >=
)时,由于这些符号在 XML 中具有特殊含义,所以需要使用 CDATA
标签将其包裹起来,避免被 XML 解析器误解为标签。
缓存
MyBatis 提供了缓存机制以提升数据查询的性能,减少数据库的压力。MyBatis 的缓存分为一级缓存和二级缓存。
1. 一级缓存
一级缓存是 MyBatis 的默认缓存机制,它的作用范围是 SqlSession。在同一个 SqlSession 中执行相同的查询时,如果之前已经执行过该查询并且结果已缓存,MyBatis 会直接从缓存中获取结果,而不会再次发起数据库查询。
- 作用范围: 同一个 SqlSession 内。
- 生效条件:
- 查询操作必须是相同的(包括参数相同)。
- SqlSession 不能被关闭。
- 没有执行增删改操作(增删改操作会刷新一级缓存)。
一级缓存是线程不安全的,因为一个 SqlSession 通常不会在多个线程之间共享。
2. 二级缓存
二级缓存是 MyBatis 提供的全局缓存机制,作用范围是 Mapper 映射级别。不同的 SqlSession 可以共享二级缓存的数据,从而在多个 SqlSession 中复用查询结果。
- 作用范围: Mapper 映射级别(同一个命名空间)。
- 生效条件:
- 必须在 MyBatis 全局配置中开启二级缓存。
- 在映射文件中指定
<cache />
元素来启用缓存。 - 查询结果对象必须是可序列化的。
- 没有执行增删改操作(增删改操作会刷新二级缓存)。
当使用不同的 SqlSession 执行相同的查询时,MyBatis 会首先检查二级缓存,如果缓存命中,则直接从缓存中读取数据;否则,会查询数据库并将结果存入缓存。
3. 二级缓存的工作机制
- 存储结构: 二级缓存默认使用基于 PerpetualCache 实现的缓存,它是一个基础的 Map 结构。可以结合不同的缓存策略如 FIFO、LRU 等来控制缓存的行为。
- 缓存刷新: 当执行更新、插入或删除操作时,对应命名空间的二级缓存会被清空。
- 可配置性: MyBatis 支持自定义缓存实现,也可以使用第三方的缓存框架,如 Ehcache。
4. 一级缓存和二级缓存的区别
项目 | 一级缓存 | 二级缓存 |
---|---|---|
作用范围 | SqlSession 内 | Mapper 映射级别(全局) |
默认开启 | 是 | 否 |
失效条件 | SqlSession 关闭 | 增删改操作 |
线程安全性 | 线程不安全 | 线程安全(可配置) |
使用场景 | 简单查询的缓存 | 复杂查询、多线程应用 |
缓存的注意事项
- 在使用二级缓存时,查询结果对象必须实现
Serializable
接口。 - 缓存并不总是适合所有场景,对于频繁变化的数据,使用缓存可能会导致数据不一致。
- 在集群环境中,使用分布式缓存解决方案(如 Redis、Ehcache)更为合适,以确保缓存的一致性。
通过合理使用 MyBatis 的一级缓存和二级缓存机制,可以显著提高数据库查询的性能,减少数据库的负载。但需要根据具体的应用场景权衡缓存的使用。
SqlSession
MyBatis 提供的用于执行 SQL 语句、获取映射器、管理事务的接口。SqlSession
表示与数据库的一次会话,它封装了对数据库的操作方法,如查询、插入、更新、删除等。
1. SqlSession
的职责
-
执行 SQL 操作:
SqlSession
提供了多种方法来执行 SQL 语句,例如selectOne
、selectList
、insert
、update
和delete
等。这些方法实际上是对数据库的操作请求。 -
获取 Mapper:
SqlSession
可以获取 Mapper 接口的实例,这些实例通过动态代理的方式将接口的方法调用转换为 SQL 请求。 -
事务管理:
SqlSession
提供了对事务的管理功能,可以手动提交事务 (commit
)、回滚事务 (rollback
),以及关闭会话 (close
)。 -
缓存管理:
SqlSession
也管理一级缓存,在同一个会话中,如果多次查询相同的数据,可能会从缓存中获取结果,而不是每次都发起数据库请求。
2. SqlSession
的生命周期
- 创建: 通过
SqlSessionFactory
创建SqlSession
对象。 - 使用: 在
SqlSession
的生命周期中,可以执行多次数据库操作(多次 SQL 请求)。 - 关闭: 当所有操作完成后,应该关闭
SqlSession
,以释放资源。
SqlSessionFactory sqlSessionFactory = ...; // 从配置或编程方式创建
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// 使用 sqlSession 执行多次数据库操作
User user = sqlSession.selectOne("com.example.mapper.UserMapper.selectUser", 1);
List<Order> orders = sqlSession.selectList("com.example.mapper.OrderMapper.selectOrdersByUserId", 1);
// 提交事务(如果需要)
sqlSession.commit();
} catch (Exception e) {
// 回滚事务(如果发生异常)
sqlSession.rollback();
} finally {
// 关闭 sqlSession
sqlSession.close();
}
SqlSession
表示一次会话,而不是单一的数据库请求。在一个SqlSession
中可以执行多个数据库请求。- 每个
SqlSession
具有独立的缓存和事务控制,关闭SqlSession
后,缓存也会被清除,未提交的事务会被回滚。 - 通常在使用
SqlSession
时,会在一个方法内创建并关闭它,确保资源被及时释放。
MyBatis在代码中添加SQL的方式
使用SqlSession
直接执行SQL:
MyBatis提供了SqlSession
对象,可以通过SqlSession
的selectList
、update
、insert
、delete
等方法执行SQL。这些SQL可以直接在Java代码中动态拼接并执行。
try (SqlSession session = sqlSessionFactory.openSession()) {
String sql = "SELECT * FROM users WHERE id = #{id}";
List<User> users = session.selectList("customSql", Collections.singletonMap("id", 1));
}
自定义Mapper
接口中的方法:
可以在Mapper接口的方法中通过@SelectProvider
、@InsertProvider
、@UpdateProvider
、@DeleteProvider
等注解来指定一个自定义的SQL构造方法。
public interface UserMapper {
@SelectProvider(type = UserSqlProvider.class, method = "selectByCustomCondition")
List<User> selectByCustomCondition(Map<String, Object> params);
}
public class UserSqlProvider {
public String selectByCustomCondition(Map<String, Object> params) {
StringBuilder sql = new StringBuilder("SELECT * FROM users WHERE 1=1 ");
if (params.get("name") != null) {
sql.append("AND name = #{name} ");
}
if (params.get("age") != null) {
sql.append("AND age = #{age} ");
}
return sql.toString();
}
}
直接在Java代码中使用拼接:
在某些复杂或者需要动态拼接SQL的场景,可以直接在Java代码中构造SQL,然后通过MyBatis执行。使用这种方法时,需要特别注意SQL注入的风险,建议对参数进行预处理或使用PreparedStatement
形式。
String sql = "SELECT * FROM users WHERE name = '" + name + "' AND age = " + age;
List<User> users = sqlSession.selectList("customSql", sql);
遇到的场景,实习期间看版本号自动修改,大致思路:
//XML 中定义 SQL 模板
<!-- UserMapper.xml -->
<update id="updateUserBase" parameterType="map">
UPDATE users
<set>
<if test="name != null">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
<!-- 版本号留作动态补充部分 -->
</set>
WHERE id = #{id}
</update>
//Java 代码中动态补充 SQL,在Java类UserSqlProvider.java中动态生成与补充版本号相关的SQL。
// UserSqlProvider.java
public class UserSqlProvider {
public String updateVersion(Map<String, Object> params) {
// 从参数中获取变量
Integer newVersion = (Integer) params.get("newVersion");
Integer oldVersion = (Integer) params.get("oldVersion");
// 动态生成版本号更新的SQL片段
StringBuilder sql = new StringBuilder();
sql.append("object_version_number = ").append(newVersion);
sql.append(" WHERE object_version_number = ").append(oldVersion);
return sql.toString();
}
}
//在UserMapper接口中,结合XML和Java代码来完成最终的SQL执行。
// UserMapper.java
public interface UserMapper {
// 使用XML中的基础SQL模板
@UpdateProvider(type = UserSqlProvider.class, method = "updateVersion")
int updateUserBase(Map<String, Object> params);
}
-
SQL自动变更:在执行
UPDATE
SQL语句时,ORM框架(如MyBatis, Hibernate)会自动检查版本号是否一致,如果不一致,则更新操作会失败,从而防止数据被其他事务修改,确保数据的一致性。 -
拦截和修改SQL:
- 在执行
UPDATE
操作时,ORM框架会拦截SQL语句,通过@VersionAudit
注解获取版本字段的当前值。 - ORM框架会将当前版本号与数据库中的版本号进行比较。如果版本号匹配,SQL语句会将版本号加1并更新记录。
- 如果版本号不匹配,更新操作会失败,通常会抛出一个并发更新异常。
- 在执行
JPA
JPA(Java Persistence API)是 Java EE(Java Enterprise Edition)中定义的一组标准规范,用于对象持久化。JPA 允许开发者使用对象来与关系数据库进行交互,而不必编写大量的 SQL 语句。JPA 通过提供一组注解和接口,实现了对象与数据库表之间的映射关系,并简化了数据持久化的操作。
JPA 的核心概念
-
实体(Entity):
- 实体类是一个轻量级的持久化领域对象,它通常与数据库中的表一一对应。每个实体类的实例代表数据库表中的一行记录。
- 实体类通过
@Entity
注解标记。
-
主键(Primary Key):
- 每个实体类都必须有一个主键,它用
@Id
注解标识。主键可以由数据库自动生成,也可以由应用程序指定。 - 主键的生成策略可以通过
@GeneratedValue
注解进行配置,如AUTO
、IDENTITY
、SEQUENCE
等。
- 每个实体类都必须有一个主键,它用
-
关系映射(Relationships):
- JPA 支持实体之间的关系映射,例如一对一、一对多、多对一、多对多。使用
@OneToOne
、@OneToMany
、@ManyToOne
、@ManyToMany
注解来定义实体之间的关系。
- JPA 支持实体之间的关系映射,例如一对一、一对多、多对一、多对多。使用
-
查询(Query):
- JPA 提供了 JPQL(Java Persistence Query Language)来查询数据库。JPQL 类似于 SQL,但操作的是实体对象而不是表。
- 你可以使用
@Query
注解编写 JPQL,也可以通过EntityManager
的方法执行查询。 -
@Query("SELECT u FROM User u WHERE u.username = :username") User findByUsername(@Param("username") String username);
-
EntityManager:
-
EntityManager
是 JPA 的核心接口,用于管理实体的生命周期、执行查询、处理事务等。它提供了保存、更新、删除、查找实体的方法。 -
EntityManager em = entityManagerFactory.createEntityManager(); em.getTransaction().begin(); em.persist(user); em.getTransaction().commit();
-
-
事务(Transaction):
- JPA 支持事务管理,通常与 JTA(Java Transaction API)结合使用。在 Spring 中,可以使用
@Transactional
注解来管理事务。
- JPA 支持事务管理,通常与 JTA(Java Transaction API)结合使用。在 Spring 中,可以使用
javax.persistence
包中的常用内容
-
实体相关注解:
@Entity
:标记一个类为 JPA 实体。@Id
:标记实体类中的主键字段。@GeneratedValue
:用于定义主键的生成策略。@Table
:用于指定实体对应的数据库表名。@Column
:用于指定实体字段对应的数据库列名。@OneToOne
,@OneToMany
,@ManyToOne
,@ManyToMany
:用于定义实体之间的关系。
-
查询相关注解:
@Query
:用于定义 JPQL(Java Persistence Query Language)查询。@NamedQuery
:用于定义命名查询。@NamedQueries
:用于定义多个命名查询。
-
事务管理:
@Transactional
:标记一个方法或类支持事务管理(通常在 Spring 框架中使用,但在 JPA 中也有类似概念)。
-
实体管理:
EntityManager
:JPA 的核心接口,用于管理实体的持久化、查询、更新和删除等操作。EntityTransaction
:用于处理事务。
-
持久化单元:
Persistence
:用于创建EntityManagerFactory
实例的类。
JPA 的优点
- 标准化:JPA 是 Java EE 的标准,多个实现如 Hibernate、EclipseLink、OpenJPA 都遵循该规范,保证了应用程序的可移植性。
- 面向对象:JPA 提供了将关系数据库表映射为 Java 对象的机制,使得数据库操作更加符合面向对象的编程风格。
- 简化开发:通过注解和面向对象的查询语言(JPQL),JPA 简化了持久化层的开发,减少了冗余的 SQL 编写。
- 事务管理:JPA 支持声明式事务管理,使得事务处理更加方便可靠。
常见的 JPA 实现:
Hibernate:
- 最流行的 JPA 实现之一,提供了丰富的功能和强大的 ORM 能力,支持多种数据库。
Spring Data JPA:
- Spring Data JPA 是 Spring 提供的 JPA 增强框架,它简化了 JPA 的使用,使得数据访问层的开发更加快捷
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
// getters and setters
}
public class JpaExample {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence-unit");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
User user = new User();
user.setUsername("john");
user.setPassword("password");
em.persist(user);
em.getTransaction().commit();
em.close();
emf.close();
}
}
Spring Data JPA
-
扩展和简化:
- Spring Data JPA 是 Spring 提供的一个扩展库,基于 JPA,旨在简化数据访问层的开发。
- 提供了对 JPA 的增强功能,包括自动实现数据访问接口、简化查询、集成 Spring 的事务管理等。
-
核心功能:
- 提供了
JpaRepository
和其他类似的接口,自动生成常见的 CRUD 操作和查询方法。 - 支持使用方法名称生成查询(例如
findByUsername
)。
- 提供了
-
配置:
- 使用 Spring Boot 可以通过自动配置简化 JPA 配置。
application.properties
或application.yml
文件用于配置数据源和 JPA 属性。
-
查询:
- 支持通过方法名称生成查询,简化了查询的编写。
- 支持
@Query
注解编写自定义 JPQL 查询。 - 提供了动态查询的功能,如 QueryDSL 和 Specifications。
-
事务管理:
- 集成了 Spring 的事务管理,支持声明式事务处理(通过
@Transactional
注解)
- 集成了 Spring 的事务管理,支持声明式事务处理(通过
//使用 JPA
public class UserRepository {
private EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence-unit");
private EntityManager em = emf.createEntityManager();
public User findByUsername(String username) {
Query query = em.createQuery("SELECT u FROM User u WHERE u.username = :username");
query.setParameter("username", username);
return (User) query.getSingleResult();
}
}
//使用 Spring Data JPA:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
对比分析
-
简化代码:
- JPA:需要手动实现数据访问逻辑和查询方法,代码量较多,特别是在执行复杂查询时。
- Spring Data JPA:通过
JpaRepository
提供了自动实现的基本 CRUD 操作,并且可以通过方法名称自动生成查询,减少了大量的样板代码。
-
查询方法:
- JPA:使用
EntityManager
手动创建查询,编写 JPQL 或 SQL。 - Spring Data JPA:可以通过方法名称自动生成查询(例如
findByUsername
),并支持使用@Query
注解定义自定义查询。
- JPA:使用
-
事务管理:
- JPA:通常依赖于 JTA 进行事务管理,需要在代码中手动管理事务。
- Spring Data JPA:集成了 Spring 的事务管理,使用
@Transactional
注解来声明事务,简化了事务管理。
-
自动配置:
- JPA:需要手动配置
EntityManagerFactory
和EntityManager
。 - Spring Data JPA:Spring Boot 提供了自动配置,简化了 JPA 配置,减少了配置代码。
- JPA:需要手动配置
-
维护和扩展:
- JPA:维护和扩展时,需要编写更多的代码来处理查询和数据访问逻辑。
- Spring Data JPA:提供了更高层次的抽象,易于维护和扩展,特别是对于常见的 CRUD 操作。
JPQL(Java Persistence Query Language)
JPQL是 JPA(Java Persistence API)定义的查询语言,用于在对象关系映射中执行数据库查询。JPQL 允许开发者使用类似于 SQL 的语法查询实体对象,而不是直接操作数据库表。它是一种面向对象的查询语言,可以操作 JPA 实体及其属性,而不是数据库表和列。
JPQL 的核心特性
-
面向对象:
- JPQL 查询操作的是实体对象及其属性,而不是数据库中的表和列。这使得查询更加符合面向对象的编程风格。
-
类似 SQL 的语法:
- JPQL 的语法与 SQL 相似,但其查询的是实体对象而非数据库表。例如,JPQL 中的
SELECT u FROM User u WHERE u.username = :username
查询的是User
实体的对象。
- JPQL 的语法与 SQL 相似,但其查询的是实体对象而非数据库表。例如,JPQL 中的
-
动态查询:
- JPQL 支持动态查询,可以在运行时构建和执行查询。
-
支持关联查询:
- JPQL 支持在实体之间进行关联查询,例如
JOIN
查询来获取关联实体的数据。
- JPQL 支持在实体之间进行关联查询,例如
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import java.util.List;
public class UserRepository {
@PersistenceContext
private EntityManager em;
public List<User> findAllUsers() {
//查询所有用户:
String jpql = "SELECT u FROM User u";
TypedQuery<User> query = em.createQuery(jpql, User.class);
return query.getResultList();
//使用参数查询,根据用户名查询用户:
String jpql = "SELECT u FROM User u WHERE u.username = :username";
TypedQuery<User> query = em.createQuery(jpql, User.class);
query.setParameter("username", username);
//使用 JOIN 查询,查询用户及其订单:
String jpql = "SELECT u FROM User u JOIN u.orders o";
TypedQuery<User> query = em.createQuery(jpql, User.class);
return query.getResultList();
//聚合函数,查询用户数量:
String jpql = "SELECT COUNT(u) FROM User u";
TypedQuery<Long> query = em.createQuery(jpql, Long.class);
return query.getSingleResult();
}
}
JPQL 语法规则
- 选择(SELECT):用于指定要查询的实体或其属性。例如,
SELECT u FROM User u
查询User
实体的所有记录。 - 条件(WHERE):用于指定查询的条件。例如,
WHERE u.username = :username
。 - 排序(ORDER BY):用于指定结果集的排序方式。例如,
ORDER BY u.username ASC
。 - 分页:通过设置
setFirstResult
和setMaxResults
方法来进行分页查询。 - 联接(JOIN):用于关联查询。例如,
JOIN u.orders o
。 - 聚合函数:如
COUNT()
、SUM()
、AVG()
、MAX()
和MIN()
。
Hibernate
Hibernate 和 Spring Data JPA 在用法上的差别主要体现在数据访问方式、配置复杂度、和开发效率上。以下是两者在用法上的主要差异:
1. 配置和初始化
-
Hibernate:
- 需要手动配置
hibernate.cfg.xml
或 Java-based 配置来指定数据库连接信息、实体类的映射、Hibernate 方言等。 - 开发者需要手动管理
SessionFactory
和Session
对象来进行数据库操作。
- 需要手动配置
-
Spring Data JPA:
- 配置更加简化,通常只需在
application.properties
或application.yml
文件中指定数据库连接信息,Spring Boot 自动配置大部分 JPA 相关内容。 - 自动管理
EntityManager
,通过 Spring 的依赖注入机制简化数据访问层的开发。
- 配置更加简化,通常只需在
2. 数据访问方式
-
Hibernate:
- 主要使用
Session
对象进行数据访问。通过Session.save()
、Session.update()
、Session.delete()
等方法进行 CRUD 操作。 - 查询语言为 HQL(Hibernate Query Language),需要手动编写查询语句。
- 主要使用
-
Spring Data JPA:
- 使用
JpaRepository
或CrudRepository
接口,无需手动编写基本的 CRUD 操作,大量常用操作都已封装好。 - 支持通过方法名称约定(query methods)自动生成查询语句,例如
findByUsername(String username)
,Spring Data JPA 自动生成 JPQL 查询。 - 自定义查询可以使用
@Query
注解,直接在接口方法上定义 JPQL 或原生 SQL 查询。
- 使用
3. 代码示例
// Hibernate Configuration
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
// Save a user
User user = new User();
user.setUsername("john");
user.setPassword("password");
session.save(user);
transaction.commit();
session.close();
4. 查询方式
-
Hibernate:
- 查询使用 HQL,语法类似 SQL,但操作的是实体类和属性。
- 需要手动创建
Query
对象并执行查询。
-
Spring Data JPA:
- 支持通过方法名称推导查询,减少手动编写查询语句的工作。
- 可以使用
@Query
注解定义复杂查询,支持 JPQL 和原生 SQL。
5. 事务管理
-
Hibernate:
- 需要手动管理事务,通常通过
Transaction
对象来控制事务的开始和提交。
- 需要手动管理事务,通常通过
-
Spring Data JPA:
- 使用 Spring 的
@Transactional
注解简化事务管理。Spring 自动处理事务的开始、提交和回滚。
- 使用 Spring 的
6. 开发效率
-
Hibernate:
- 灵活性高,但需要编写更多的配置和样板代码。
- 适合需要深入控制和优化的场景。
-
Spring Data JPA:
- 提供了大量的自动化功能,开发效率高。
- 适合大多数常见的 CRUD 和查询场景,极大地减少了手动编写代码的工作量。
HQL(Hibernate Query Language)和 JPQL(Java Persistence Query Language)在很多方面非常相似,但它们也有一些关键区别。它们都是面向对象的查询语言,用于在对象关系映射(ORM)框架中编写查询语句。然而,HQL 是特定于 Hibernate 的查询语言,而 JPQL 是 JPA(Java Persistence API)的标准查询语言。