基本操作
CRUD
在MyBatis中,Mapper接口用于定义基本的CRUD(Create, Read, Update, Delete)操作。可以使用XML配置文件或Java注解来定义这些操作。以下是使用这两种方式定义基本CRUD操作的示例。
使用XML配置文件
1. 定义Mapper接口
public interface UserMapper {
User selectUser(int id);
List<User> selectAllUsers();
void insertUser(User user);
void updateUser(User user);
void deleteUser(int id);
}
2. 配置Mapper XML文件
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectUser" resultType="com.example.domain.User" parameterType="int">
SELECT * FROM users WHERE id = #{id}
</select>
<select id="selectAllUsers" resultType="com.example.domain.User">
SELECT * FROM users
</select>
<insert id="insertUser" parameterType="com.example.domain.User">
INSERT INTO users (name, email) VALUES (#{name}, #{email})
</insert>
<update id="updateUser" parameterType="com.example.domain.User">
UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}
</update>
<delete id="deleteUser" parameterType="int">
DELETE FROM users WHERE id = #{id}
</delete>
</mapper>
使用注解
1. 定义Mapper接口并使用注解
import org.apache.ibatis.annotations.*;
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User selectUser(int id);
@Select("SELECT * FROM users")
List<User> selectAllUsers();
@Insert("INSERT INTO users (name, email) VALUES (#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insertUser(User user);
@Update("UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}")
void updateUser(User user);
@Delete("DELETE FROM users WHERE id = #{id}")
void deleteUser(int id);
}
示例代码说明
- Select操作:用于查询单条记录或多条记录,使用
@Select
注解或<select>
标签。 - Insert操作:用于插入记录,使用
@Insert
注解或<insert>
标签。通常会使用@Options
注解来获取生成的主键。 - Update操作:用于更新记录,使用
@Update
注解或<update>
标签。 - Delete操作:用于删除记录,使用
@Delete
注解或<delete>
标签。
使用Mapper接口
public class MyBatisExample {
public static void main(String[] args) {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 插入用户
User newUser = new User();
newUser.setName("John");
newUser.setEmail("john@example.com");
mapper.insertUser(newUser);
session.commit();
// 查询用户
User user = mapper.selectUser(newUser.getId());
System.out.println(user);
// 更新用户
user.setName("Jane");
user.setEmail("jane@example.com");
mapper.updateUser(user);
session.commit();
// 删除用户
mapper.deleteUser(user.getId());
session.commit();
// 查询所有用户
List<User> users = mapper.selectAllUsers();
users.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
}
动态SQL
MyBatis 提供了一组动态 SQL 标签,这些标签可以帮助生成动态 SQL 语句,使得 SQL 语句更灵活和可重用。以下是 MyBatis 动态 SQL 标签及其用法的详细解释:
1. <if>
标签
<if>
标签用于根据条件动态地包含 SQL 片段。
示例
<select id="selectUser" parameterType="map" resultType="User">
SELECT * FROM users
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="name != null">
AND name = #{name}
</if>
</where>
</select>
2. <choose>
, <when>
, <otherwise>
标签
这些标签类似于 Java 中的 switch
语句, <choose>
标签用于选择一个条件进行处理。
示例
<select id="selectUser" parameterType="map" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="id != null">
AND id = #{id}
</when>
<when test="name != null">
AND name = #{name}
</when>
<otherwise>
AND email = #{email}
</otherwise>
</choose>
</where>
</select>
3. <trim>
标签
<trim>
标签用于处理动态 SQL 中的冗余字符,如多余的空格、逗号、AND
或 OR
关键字等。它允许你在 SQL 语句的开头或结尾添加或移除某些内容,这对于构建更干净、更有效的 SQL 语句特别有用。<trim>
标签支持以下属性:
-
prefix:
这个属性允许你在<trim>
标签内生成的 SQL 语句前添加一个前缀。例如,你可以添加"WHERE"
或"SET"
等关键词作为前缀。 -
prefixOverrides:
这个属性指定了应该从最终 SQL 语句中移除的前缀内容。通常用于移除可能由<trim>
标签内其他元素(如<if>
)生成的多余字符。例如,如果你的 SQL 语句开始于多个AND
或OR
,你可以设置prefixOverrides="AND,OR"
来移除这些多余的关键词。 -
suffix:
这个属性允许你在<trim>
标签内生成的 SQL 语句末尾添加一个后缀。例如,你可能会添加")"
或,
等字符。 -
suffixOverrides:
类似于prefixOverrides
,这个属性指定了应该从最终 SQL 语句中移除的后缀内容。例如,如果你的 SQL 结尾有多余的逗号,你可以通过设置suffixOverrides=","
来移除它。
下面是一个使用 <trim>
标签的示例,展示如何构建一个带有动态 WHERE
子句的 SQL 查询:
<select id="findUsers" parameterType="map" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND|OR">
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="username != null">
AND username = #{username}
</if>
<if test="email != null">
AND email = #{email}
</if>
</trim>
</select>
在这个例子中:
prefix="WHERE"
将在动态条件开始时添加WHERE
关键词。prefixOverrides="AND|OR"
将确保如果动态条件为空,WHERE
关键词不会被添加,并且还会移除任何不必要的AND
或OR
关键词。
使用 <trim>
标签可以帮助你编写更加健壮和可读性强的动态 SQL 语句。
4. <where>
标签
<where>
标签用于自动添加 WHERE
关键字,并自动去掉多余的 AND
或 OR
。
⚠️注意:只会自动去掉多余的,并不会自动添加。
where的自动去掉多余AND是基于trim实现的
🤔️ 问题:trim支持添加前缀,为什么where却不支持自动添加呢?
正确示例
<select id="selectUser" parameterType="map" resultType="User">
SELECT * FROM users
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="name != null">
AND name = #{name}
</if>
</where>
</select>
示例 - if中缺少AND
假设我们在 <if>
语句中没有包含 AND
:
<select id="selectUser" parameterType="map" resultType="User">
SELECT * FROM users
<where>
<if test="id != null">
id = #{id}
</if>
<if test="name != null">
name = #{name}
</if>
</where>
</select>
如果 id
和 name
都不为 null
,生成的 SQL 语句会是:
SELECT * FROM users WHERE id = 1 name = 'John'
这会产生语法错误,因为 id = 1
和 name = 'John'
之间缺少 AND
。
5. <set>
标签
<set>
标签用于动态生成 SET
语句,适用于 UPDATE
操作,并去掉多余的逗号。
示例
<update id="updateUser" parameterType="User">
UPDATE users
<set>
<if test="name != null">
name = #{name},
</if>
<if test="email != null">
email = #{email},
</if>
</set>
WHERE id = #{id}
</update>
6. 其他动态 SQL 标签
<foreach>
标签
<foreach>
标签用于处理集合类型的参数(如列表、数组),用于批量操作。
示例
<select id="selectUsers" parameterType="list" resultType="User">
SELECT * FROM users WHERE id IN
<foreach item="id" collection="list" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<bind>
标签
<bind>
标签用于创建一个新的绑定变量。
示例
<select id="selectUser" parameterType="string" resultType="User">
<bind name="pattern" value="'%' + name + '%'" />
SELECT * FROM users WHERE name LIKE #{pattern}
</select>
动态 SQL 示例
综合示例
<select id="selectUser" parameterType="map" resultType="User">
SELECT * FROM users
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
<update id="updateUser" parameterType="User">
UPDATE users
<set>
<if test="name != null">
name = #{name},
</if>
<if test="email != null">
email = #{email},
</if>
</set>
WHERE id = #{id}
</update>
动态SQL应用和常见问题
应用场景:
-
条件筛选:根据用户输入的不同条件动态生成 WHERE 子句,仅包含实际需要的条件。例如,在进行查询时,只有当用户提供了搜索关键词时才添加相应的过滤条件。
-
分页查询:根据分页参数动态调整 LIMIT 或 OFFSET(或在 SQL Server 中的 TOP 和 OFFSET FETCH)子句,实现数据分页功能。
-
动态排序:根据用户选择的排序字段和顺序动态生成 ORDER BY 子句。
-
动态列选择:根据需求选择返回的列,而不是总是返回所有列,提高查询效率。
-
IN 子句的动态生成:当需要在 IN 条件中包含一个可变数量的参数列表时,动态生成 IN 子句。
-
嵌套查询条件:根据条件是否满足,动态决定是否执行某个子查询或 JOIN 操作。
常见问题:
-
性能问题:不当使用动态 SQL 可能导致生成的 SQL 语句过于复杂,影响数据库执行效率。特别是当使用过多的
<if>
、<choose>
等标签时,生成的 SQL 可能难以被数据库优化。 -
SQL 注入:虽然 MyBatis 自身对参数进行了预编译处理,减少了 SQL 注入的风险,但如果动态 SQL 逻辑错误地拼接字符串作为 SQL 参数,仍然可能引入安全漏洞。
-
可读性和维护性:过度使用动态 SQL 可能使映射文件变得复杂难懂,降低了代码的可读性和维护性。
-
类型转换错误:动态 SQL 中的条件判断可能涉及不同类型变量的比较,如果不注意类型转换,可能会遇到类型不匹配的问题。
-
标签使用不当:不正确使用 MyBatis 提供的动态 SQL 标签(如
<if>
、<foreach>
、<choose>
等)可能导致预期之外的 SQL 生成或执行错误。
为了避免这些问题,建议遵循以下最佳实践:
- 尽量保持 SQL 的简洁和明确,避免不必要的复杂动态逻辑。
- 使用预编译参数而非字符串拼接来防止 SQL 注入。
- 适时对动态 SQL 进行单元测试和性能测试,确保其按预期工作且性能良好。
- 在设计动态 SQL 时,保持映射文件的清晰和逻辑的可读性,必要时添加注释说明。
- 熟悉并正确运用 MyBatis 提供的各种动态 SQL 标签。
mybatis 动态处理where源码
在 MyBatis 源代码中,<where>
标签的实现位于 org.apache.ibatis.scripting.xmltags
包中,特别是在 WhereSqlNode
类中。这个类负责处理 <where>
标签的具体逻辑。
MyBatis 源代码中相关类的位置
-
WhereSqlNode
类:处理<where>
标签的逻辑。- 包路径:
org.apache.ibatis.scripting.xmltags
- 类文件:
WhereSqlNode.java
- 包路径:
-
TrimSqlNode
类:WhereSqlNode
类继承自TrimSqlNode
类,TrimSqlNode
负责处理动态 SQL 中的前缀和后缀逻辑。- 包路径:
org.apache.ibatis.scripting.xmltags
- 类文件:
TrimSqlNode.java
- 包路径:
WhereSqlNode
类的代码
以下是 WhereSqlNode
类的简化代码示例:
package org.apache.ibatis.scripting.xmltags;
public class WhereSqlNode extends TrimSqlNode {
private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", PREFIX_LIST, null, null);
}
}
WhereSqlNode
通过继承 TrimSqlNode
类,并传递 WHERE
作为前缀,自动处理多余的 AND
或 OR
关键字。
TrimSqlNode
类的代码
以下是 TrimSqlNode
类的简化代码示例:
package org.apache.ibatis.scripting.xmltags;
public class TrimSqlNode implements SqlNode {
private final Configuration configuration;
private final SqlNode contents;
private final String prefix;
private final List<String> prefixOverrides;
public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixOverrides, String suffix, List<String> suffixOverrides) {
this.configuration = configuration;
this.contents = contents;
this.prefix = prefix;
this.prefixOverrides = prefixOverrides;
}
@Override
public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
boolean result = contents.apply(filteredDynamicContext);
filteredDynamicContext.applyAll();
return result;
}
private class FilteredDynamicContext extends DynamicContext {
private DynamicContext delegate;
private boolean prefixApplied;
public FilteredDynamicContext(DynamicContext delegate) {
super(configuration, delegate.getBindings());
this.delegate = delegate;
}
public void applyAll() {
if (prefix != null) {
String sql = getSql();
if (sql.length() > 0) {
applyPrefix(sql);
}
}
}
private void applyPrefix(String sql) {
if (!prefixApplied) {
String trimmedSql = sql.trim();
if (trimmedSql.length() > 0) {
for (String prefixToOverride : prefixOverrides) {
if (trimmedSql.toUpperCase().startsWith(prefixToOverride)) {
trimmedSql = trimmedSql.substring(prefixToOverride.length());
break;
}
}
delegate.appendSql(prefix + " " + trimmedSql);
prefixApplied = true;
}
}
}
}
}
TrimSqlNode
负责处理 SQL 语句的前缀和后缀逻辑,确保生成的 SQL 语句格式正确。
高级特性
一级缓存和二级缓存
MyBatis 提供了两级缓存机制:一级缓存(PerpetualCache)和二级缓存(Ehcache、Hazelcast等)。下面详细介绍这两级缓存的工作原理、源码分析以及配置,并使用 Mermaid 图标识缓存作用的位置。
一级缓存(PerpetualCache)
工作原理
- 范围:一级缓存是 SqlSession 级别的缓存,即同一个 SqlSession 中的查询结果会被缓存,再次查询相同的数据会从缓存中获取。
- 失效:当 SqlSession 执行
INSERT
、UPDATE
、DELETE
操作,或调用clearCache()
方法时,一级缓存会被清空。
源码分析
一级缓存的实现类是 PerpetualCache
,它被 CachingExecutor
用来缓存查询结果。
主要代码
PerpetualCache
的实现:
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public int getSize() {
return cache.size();
}
}
CachingExecutor
的部分实现:
public class CachingExecutor implements Executor {
private final Executor delegate;
private final Map<CacheKey, Object> localCache = new HashMap<>();
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
if (localCache.containsKey(key)) {
return (List<E>) localCache.get(key);
} else {
List<E> result = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
localCache.put(key, result);
return result;
}
}
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
localCache.clear();
}
@Override
public void rollback(boolean required) throws SQLException {
delegate.rollback(required);
localCache.clear();
}
}
配置
一级缓存默认开启,无需额外配置。只要使用相同的 SqlSession
,一级缓存就会自动生效。
二级缓存
工作原理
- 范围:二级缓存是 Mapper 级别的缓存,即同一个 Mapper 的所有 SqlSession 共享这一级缓存。
- 配置:需要在 MyBatis 配置文件中显式配置并开启。
- 失效:当某个表的数据发生变更(
INSERT
、UPDATE
、DELETE
),相关缓存会失效。
源码分析
二级缓存的实现依赖于 Cache
接口和相应的实现类(如 EhcacheCache
、RedisCache
等)。
主要代码
Cache
接口定义:
public interface Cache {
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
}
配置文件中开启二级缓存:
<cache/>
示例:
<mapper namespace="com.example.mapper.UserMapper">
<cache/>
<select id="selectUser" parameterType="map" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
二级缓存配置
在 MyBatis 全局配置文件(mybatis-config.xml
)中配置缓存:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
在每个 Mapper XML 文件中启用二级缓存:
<cache/>
图示
- Application:应用程序调用 MyBatis。
- SqlSession:MyBatis 会话。
- Executor:执行器,负责执行 SQL 语句。
- CachingExecutor:带缓存功能的执行器。
- PerpetualCache:一级缓存,SqlSession 级别的缓存。
- Cache:二级缓存,Mapper 级别的缓存。
- Database:数据库,最终的数据源。
缓存生命周期&替换策略&持久化&使用注意
MyBatis 缓存的生命周期和持久化
一级缓存
- 生命周期:一级缓存是基于
SqlSession
的。也就是说,它的生命周期和SqlSession
是相同的。当SqlSession
被关闭或清理时,一级缓存中的数据也会被清除。 - 持久化:一级缓存不会持久化,它只存在于
SqlSession
的内存中。
二级缓存
- 生命周期:二级缓存是基于
Mapper
的。只要Mapper
存在,二级缓存的数据就会保留。 - 持久化:二级缓存可以配置为持久化,这取决于缓存实现。如果使用的是像 Ehcache、Redis 等支持持久化的缓存实现,数据可以被持久化到磁盘或其他存储介质。
缓存替换策略
一级缓存
- 一级缓存没有特别的替换策略,因为它的生命周期和
SqlSession
是相同的,一旦SqlSession
关闭,缓存就会被清空。
二级缓存
- MyBatis 本身没有定义具体的替换策略,而是依赖于具体的缓存实现。例如,使用 Ehcache 时,可以配置 LRU(最近最少使用)、LFU(最少频率使用)等策略。
配置二级缓存持久化的示例
使用 Ehcache
-
引入依赖:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.1.0</version> </dependency>
-
配置 Ehcache:
在ehcache.xml
文件中配置持久化和缓存策略:<ehcache> <diskStore path="java.io.tmpdir"/> <cache name="com.example.mapper.UserMapper" maxEntriesLocalHeap="1000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" diskSpoolBufferSizeMB="20" maxEntriesLocalDisk="10000000" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LFU"> <persistence strategy="localTempSwap"/> </cache> </ehcache>
-
MyBatis 配置:
在 MyBatis 的全局配置文件mybatis-config.xml
中启用二级缓存并指定缓存实现:<settings> <setting name="cacheEnabled" value="true"/> </settings> <typeAliases> <typeAlias alias="Ehcache" type="org.mybatis.caches.ehcache.EhcacheCache"/> </typeAliases>
-
Mapper 配置:
在每个 Mapper XML 文件中启用二级缓存:<mapper namespace="com.example.mapper.UserMapper"> <cache type="Ehcache"/> <select id="selectUser" parameterType="map" resultType="User"> SELECT * FROM users WHERE id = #{id} </select> </mapper>
实际生产使用缓存的最佳实践
-
选择合适的缓存实现:
- 根据业务需求选择合适的缓存实现(如 Ehcache、Redis、Hazelcast 等)。
- 确保缓存实现能满足系统的性能和持久化需求。
-
合理设置缓存的生命周期:
- 根据数据更新频率和访问频率设置合理的缓存失效时间。
- 对于不常更新但访问频繁的数据,可以设置较长的失效时间。
-
避免缓存雪崩和击穿:
- 缓存雪崩:大量缓存同时失效导致数据库瞬间压力剧增。解决方案是为不同数据设置不同的过期时间。
- 缓存击穿:某个热点数据在失效的瞬间,有大量请求同时访问该数据。解决方案是使用互斥锁或热点数据预热。
-
监控和优化缓存性能:
- 使用监控工具监控缓存的命中率、使用率等性能指标。
- 根据监控结果不断优化缓存配置和使用策略。
-
数据一致性:
- 确保缓存中的数据与数据库中的数据一致性,特别是在数据更新时。
- 可以使用消息队列或事件驱动机制来同步缓存和数据库的数据。
CacheBuilder
org.apache.ibatis.mapping.CacheBuilder
是 MyBatis 中用于构建缓存对象的类。它提供了一系列的参数和方法来配置缓存的行为。这些参数直接影响缓存的性能、持久化和替换策略等。以下是 CacheBuilder
的各个参数及其含义,并说明它们如何最终影响缓存。
CacheBuilder 各个参数的含义
package org.apache.ibatis.mapping;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.decorators.*;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.Alias;
import java.util.Properties;
public class CacheBuilder {
private final String id;
private Class<? extends Cache> implementation;
private Class<? extends Cache> decorator;
private Integer size;
private Long clearInterval;
private boolean readWrite;
private Properties properties;
private boolean blocking;
public CacheBuilder(String id) {
this.id = id;
this.implementation = PerpetualCache.class;
this.decorator = LruCache.class;
}
public CacheBuilder implementation(Class<? extends Cache> implementation) {
this.implementation = implementation;
return this;
}
public CacheBuilder addDecorator(Class<? extends Cache> decorator) {
this.decorator = decorator;
return this;
}
public CacheBuilder size(Integer size) {
this.size = size;
return this;
}
public CacheBuilder clearInterval(Long clearInterval) {
this.clearInterval = clearInterval;
return this;
}
public CacheBuilder readWrite(boolean readWrite) {
this.readWrite = readWrite;
return this;
}
public CacheBuilder blocking(boolean blocking) {
this.blocking = blocking;
return this;
}
public CacheBuilder properties(Properties properties) {
this.properties = properties;
return this;
}
public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : Arrays.asList(LruCache.class, SoftCache.class, WeakCache.class, ScheduledCache.class, SerializedCache.class, LoggingCache.class, SynchronizedCache.class, TransactionalCache.class, BlockingCache.class)) {
if (decorator.equals(this.decorator)) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
}
}
return cache;
}
private void setDefaultImplementations() {
if (implementation == null) {
implementation = PerpetualCache.class;
if (decorator == null) {
decorator = LruCache.class;
}
}
}
private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
Constructor<? extends Cache> cacheConstructor = cacheClass.getConstructor(String.class);
return cacheConstructor.newInstance(id);
}
private void setCacheProperties(Cache cache) {
if (properties != null) {
MetaObject metaCache = SystemMetaObject.forObject(cache);
for (Object key : properties.keySet()) {
String propertyName = (String) key;
String value = properties.getProperty(propertyName);
metaCache.setValue(propertyName, value);
}
}
}
private Cache newCacheDecoratorInstance(Class<? extends Cache> cacheClass, Cache base) {
Constructor<? extends Cache> cacheConstructor = cacheClass.getConstructor(Cache.class);
return cacheConstructor.newInstance(base);
}
}
各个参数的含义
- id:缓存的唯一标识符,一般是 Mapper 的命名空间。
- implementation:缓存的基础实现类,默认是
PerpetualCache
。该类提供最基本的缓存功能。 - decorator:缓存装饰器类,用于增强基础缓存的功能,如 LRU、FIFO 等。默认是
LruCache
。 - size:缓存大小,指定缓存能够存储的对象数量上限。适用于基于容量限制的缓存策略。
- clearInterval:缓存清理间隔时间,以毫秒为单位。某些缓存实现可能会定期清理过期或不再使用的缓存项。
- readWrite:指定缓存是否为可读写模式。可读写模式下,缓存存储的是对象的序列化副本,以防止脏读。
- properties:缓存配置的属性。可以为缓存的实现类或装饰器传递特定的配置信息。
- blocking:指定缓存是否为阻塞模式。在阻塞模式下,当缓存中没有命中时,其他线程会等待当前线程加载完毕再返回。
缓存的影响
这些参数会直接影响缓存的行为和性能。例如:
- 缓存实现和装饰器:决定了缓存的基础功能和增强功能,如是否使用 LRU 策略、软引用缓存等。
- 缓存大小:控制缓存的容量,避免内存溢出,但也可能导致频繁的缓存替换。
- 清理间隔:控制缓存的刷新频率,平衡缓存的实时性和性能。
- 读写模式:影响缓存的一致性和性能,读写模式可能导致较高的序列化和反序列化开销。
- 阻塞模式:防止缓存穿透,但可能导致线程阻塞,影响系统的并发性能。
实际生产使用缓存的最佳实践
-
选择合适的缓存实现:
- 根据业务需求选择适合的缓存实现和装饰器组合,如
LruCache
、Ehcache
、RedisCache
等。
- 根据业务需求选择适合的缓存实现和装饰器组合,如
-
配置合理的缓存大小:
- 根据系统的内存情况和数据访问频率设置缓存大小,避免缓存过大导致内存不足或过小导致缓存命中率低。
-
设置合适的清理间隔:
- 根据数据更新频率和业务需求设置清理间隔,确保缓存中的数据足够新鲜且不过于频繁清理。
-
使用读写模式:
- 在需要防止脏读的场景下使用读写模式,但要注意序列化和反序列化的性能开销。
-
启用阻塞模式:
- 在高并发访问且缓存穿透可能导致数据库压力过大的场景下使用阻塞模式,但要注意可能的线程阻塞问题。
自定义缓存
在 MyBatis 中,可以通过实现自定义的缓存来扩展其缓存机制。默认情况下,MyBatis 使用的是基于 PerpetualCache 的一级缓存和基于 LruCache 的二级缓存。如果需要自定义缓存,可以按照以下步骤进行:
实现自定义缓存
-
创建自定义缓存类:
首先,需要创建一个类来实现自定义的缓存,该类需要继承自
org.apache.ibatis.cache.Cache
接口。public class MyCustomCache implements Cache { private final String id; public MyCustomCache(String id) { this.id = id; } @Override public String getId() { return id; } @Override public void putObject(Object key, Object value) { // 实现缓存对象的放置逻辑 // 示例:将 key-value 对放入自定义的缓存中 } @Override public Object getObject(Object key) { // 实现从缓存中获取对象的逻辑 // 示例:根据 key 从自定义缓存中获取对象 return null; } @Override public Object removeObject(Object key) { // 实现从缓存中移除对象的逻辑 // 示例:根据 key 从自定义缓存中移除对象 return null; } @Override public void clear() { // 清空缓存 // 示例:清空自定义缓存中的所有对象 } @Override public int getSize() { // 获取缓存中存储对象的数量 // 示例:返回自定义缓存中对象的数量 return 0; } @Override public ReadWriteLock getReadWriteLock() { // 返回用于控制缓存的读写锁 // 示例:返回一个自定义的读写锁 return null; } }
在实现缓存类时,需要根据实际需求实现
putObject
、getObject
、removeObject
、clear
等方法来管理缓存中的对象,并且可以选择是否实现getReadWriteLock
方法来支持并发控制。 -
配置 MyBatis 使用自定义缓存:
在 MyBatis 的配置文件
mybatis-config.xml
中配置使用自定义缓存:<cache type="com.example.MyCustomCache"/>
这里的
com.example.MyCustomCache
是你实现的自定义缓存类的完整路径名。 -
注册自定义缓存(可选):
如果不在 XML 中配置,还可以在 Java 代码中注册自定义缓存:
// 在代码中注册自定义缓存 Configuration configuration = new Configuration(); configuration.addCache(new MyCustomCache("com.example.MyCustomCache"));
注意事项
- 并发控制:自定义缓存需要考虑并发访问时的线程安全性,可以使用合适的读写锁来管理缓存的并发访问。
- 缓存清理策略:需要根据业务需求实现缓存对象的清理策略,以控制缓存对象的数量和存储时长。
- 性能考虑:自定义缓存的实现需要考虑性能,尽量避免实现过于复杂或低效的缓存逻辑,以保证系统的性能表现。
分页查询
在 MyBatis 中实现分页查询主要有两种方式:使用 RowBounds
对象和利用分页插件。
1. 使用 RowBounds
RowBounds
是 MyBatis 提供的一个简单分页参数对象,它通过在查询时传入 RowBounds
参数来实现物理分页。RowBounds
包含两个属性:offset
和 limit
,分别表示从第几条记录开始查询以及需要查询多少条记录。
示例:
List<User> users = sqlSession.selectList("com.example.mapper.UserMapper.selectAllUsers", null, new RowBounds(10, 20));
在这个例子中,将会从第 11 条记录开始(索引从0开始),查询20条用户记录。
缺点:
RowBounds
的分页逻辑实现在客户端,需要一次性查询所有数据到内存中,然后再进行截取,当数据量大时,可能会造成内存溢出。- 它没有利用数据库的分页功能,性能较差。
2. 使用分页插件
为了更高效地实现分页,推荐使用分页插件。MyBatis 提供了一个官方推荐的分页插件 PageHelper
,但请注意,PageHelper
并不是 MyBatis 核心的一部分,而是作为一个外部插件存在。
安装与配置:
首先,需要在项目中引入 PageHelper
的依赖。然后,在 MyBatis 的配置文件中添加插件配置。
示例:
在 Mapper 接口中定义方法:
List<User> selectAllUsers(@Param("name") String name, Page page);
在 Service 层使用:
PageHelper.startPage(pageNum, pageSize); // pageNum 是当前页码,pageSize 是每页大小
List<User> users = userMapper.selectAllUsers(null);
PageInfo<User> pageInfo = new PageInfo<>(users);
优点:
- 直接在 SQL 语句中生成 LIMIT 或 ROW_NUMBER 等分页关键字,利用数据库的分页能力,减少数据传输量,提升性能。
- 配置简单,易于使用。
缺点:
- 需要引入额外的依赖。
- 对于某些特定的数据库方言支持可能需要额外配置。
PageHelper默认排序字段&增删数据后如何避免数据重复读
PageHelper 默认情况下并不指定排序字段,这意味着如果你不显式地在SQL查询中指定ORDER BY子句,那么分页查询的结果可能是无序的。因此,为了保证分页数据的连贯性和避免前后两页数据出现重叠或混乱,你应该明确指定一个或多个排序字段。
例如,如果你想要按照ID字段升序排序,你可以这样做:
PageHelper.orderBy("id ASC");
List<User> users = userMapper.selectAllUsers();
确保每次分页查询时都应用了相同的排序规则,这样即使数据中有增删,只要排序字段和顺序保持一致,相邻页码之间的数据应该是连续且不重叠的。
对于数据增删导致的分页问题,具体来说:
- 数据删除:如果在查询第一页后,数据集中有记录被删除,但这些记录不在第一页显示范围内,那么第一页的数据不会变化,后续页面的记录会自然前移填补空缺,由于有排序保证,不会出现重复数据。
- 数据插入:新插入的数据,如果其排序字段的值位于已查询页之间,理论上新数据可能会影响到前后两页的边界。为了精确控制分页,特别是在实时性要求高的场景,可以考虑以下策略:
- 在查询下一页之前,依据上一页最后一条记录的排序字段值作为查询条件,比如
where id > lastIdFromPreviousPage
,这样可以确保新插入的数据不会导致前后页数据重叠。 - 如果使用的是偏移量分页(即
LIMIT offset, limit
),并且无法避免数据重叠,可能需要在获取新页数据后在客户端去重。
- 在查询下一页之前,依据上一页最后一条记录的排序字段值作为查询条件,比如
多表关联映射方式
MyBatis 是一个支持自定义 SQL、存储过程以及高级映射的持久层框架,它提供了多种方式来映射一对一、一对多和多对多关系。下面是一些常用的映射方式:
一对一关系映射
假设有两个表 author
和 book
,一个作者可以写多本书,但每本书只能有一个作者。
-
嵌套查询(Nested Queries):使用嵌套查询来映射一对一关系。
<!-- 在 Author 的映射文件中 --> <resultMap id="authorMap" type="Author"> <id property="id" column="author_id"/> <result property="name" column="author_name"/> <result property="bio" column="author_bio"/> <result property="book" column="book_id" select="selectBookById"/> </resultMap> <select id="selectAuthorById" resultMap="authorMap"> SELECT author_id, author_name, author_bio, book_id FROM author WHERE author_id = #{id} </select> <!-- 在 Book 的映射文件中 --> <resultMap id="bookMap" type="Book"> <id property="id" column="book_id"/> <result property="title" column="book_title"/> <result property="isbn" column="book_isbn"/> <!-- other properties --> </resultMap> <select id="selectBookById" resultMap="bookMap"> SELECT book_id, book_title, book_isbn FROM book WHERE book_id = #{book_id} </select>
在
authorMap
中,使用select="selectBookById"
来指定嵌套查询,通过book_id
来关联作者和书籍信息。 -
嵌套结果(Nested Results):将书籍信息直接嵌套在作者的查询结果中。
<resultMap id="authorMap" type="Author"> <id property="id" column="author_id"/> <result property="name" column="author_name"/> <result property="bio" column="author_bio"/> <association property="book" column="book_id" javaType="Book"> <id property="id" column="book_id"/> <result property="title" column="book_title"/> <result property="isbn" column="book_isbn"/> </association> </resultMap>
一对多关系映射
假设有两个表 department
和 employee
,一个部门可以有多个员工。
-
集合映射(Collection Mapping):使用集合来映射一对多关系。
<resultMap id="departmentMap" type="Department"> <id property="id" column="dept_id"/> <result property="name" column="dept_name"/> <collection property="employees" ofType="Employee" column="dept_id" select="selectEmployeesByDeptId"/> </resultMap> <select id="selectDepartmentById" resultMap="departmentMap"> SELECT dept_id, dept_name FROM department WHERE dept_id = #{id} </select> <resultMap id="employeeMap" type="Employee"> <id property="id" column="emp_id"/> <result property="name" column="emp_name"/> <!-- other properties --> </resultMap> <select id="selectEmployeesByDeptId" resultMap="employeeMap"> SELECT emp_id, emp_name FROM employee WHERE dept_id = #{dept_id} </select>
在
departmentMap
中使用collection
元素来指定集合映射,通过dept_id
关联部门和员工信息。
多对多关系映射
假设有两个表 student
和 course
,一个学生可以选修多门课程,一门课程也可以被多个学生选修。
-
中间表映射(Intermediate Table Mapping):使用中间表来映射多对多关系。
<resultMap id="studentMap" type="Student"> <id property="id" column="student_id"/> <result property="name" column="student_name"/> <collection property="courses" ofType="Course" column="course_id" select="selectCoursesByStudentId"/> </resultMap> <select id="selectStudentById" resultMap="studentMap"> SELECT student_id, student_name FROM student WHERE student_id = #{id} </select> <resultMap id="courseMap" type="Course"> <id property="id" column="course_id"/> <result property="name" column="course_name"/> <!-- other properties --> </resultMap> <select id="selectCoursesByStudentId" resultMap="courseMap"> SELECT c.course_id, c.course_name FROM course c JOIN student_course sc ON c.course_id = sc.course_id WHERE sc.student_id = #{student_id} </select>
在
studentMap
中使用collection
元素来指定多对多关系的映射,通过中间表student_course
来关联学生和课程信息。
resultMap处理复杂结果映射
在 MyBatis 中,<resultMap>
标签被用来描述如何将查询结果映射到 Java 对象,特别是当涉及到复杂的映射关系,如一对一、一对多或多对多关联时。下面是如何使用 <resultMap>
定义复杂结果集映射的几个关键点:
基本结构
<resultMap id="resultMapId" type="com.example.YourEntityClass">
<!-- 结果映射条目 -->
</resultMap>
id
属性是 resultMap 的唯一标识符,用于在其他地方引用。type
属性指定这个结果映射将应用于哪个 Java 类。
常用映射条目
<id>
: 用于映射主键字段。<result>
: 用于映射普通字段。- 集合映射:
<association>
: 用于一对一关联映射。<collection>
: 用于一对多或多对一关联映射。
一对一关联()
假设有一个 User
类和一个 Address
类,一个 User
有一个 Address
。
<resultMap id="UserResultMap" type="com.example.User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<association property="address" javaType="com.example.Address" resultMap="AddressResultMap"/>
</resultMap>
<resultMap id="AddressResultMap" type="com.example.Address">
<id property="id" column="address_id"/>
<result property="city" column="city"/>
<result property="street" column="street"/>
</resultMap>
一对多关联()
如果一个 User
可以有多个 Order
,则可以使用 <collection>
。
<resultMap id="UserWithOrdersResultMap" type="com.example.User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="com.example.Order" resultMap="OrderResultMap"/>
</resultMap>
<resultMap id="OrderResultMap" type="com.example.Order">
<id property="id" column="order_id"/>
<result property="productName" column="product_name"/>
<result property="amount" column="amount"/>
</resultMap>
复杂情况下的映射技巧
- 嵌套
<association>
和<collection>
:可以在<association>
或<collection>
内部继续嵌套其他<association>
或<collection>
,以处理更深层次的关联关系。 - 使用
<discriminator>
进行类型区分:当一个结果集中可能映射到多个 Java 类型时,可以使用<discriminator>
根据某个字段的值来决定具体映射到哪个类。 - 使用
<select>
子查询:在<association>
或<collection>
中,可以使用<select>
子元素执行一个单独的 SQL 查询来获取关联对象,这称为延迟加载。
discriminator
在 MyBatis 中,<discriminator>
元素用于处理继承关系的类映射,它可以根据某个字段的值来决定实例化哪个子类。下面是一个使用 <discriminator>
的示例,假设我们有一个基类 Person
和两个子类 Student
和 Teacher
,并且数据库中有一个 person
表,其中有一个字段 type
用于区分学生和教师。
数据库表结构示例
CREATE TABLE person (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
type CHAR(1) CHECK (type IN ('S', 'T')) -- S 代表学生, T 代表教师
-- 其他字段...
);
Java 类结构
public class Person {
private int id;
private String name;
private int age;
// getters & setters
}
public class Student extends Person {
private String studentId;
// getters & setters
}
public class Teacher extends Person {
private String subject;
// getters & setters
}
MyBatis 映射文件
在 Person
的 resultMap 中使用 <discriminator>
来区分 Student
和 Teacher
。
<resultMap id="PersonResultMap" type="com.example.Person">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
<discriminator javaType="char" column="type">
<case value="S" resultMap="StudentResultMap"/>
<case value="T" resultMap="TeacherResultMap"/>
</discriminator>
</resultMap>
<resultMap id="StudentResultMap" type="com.example.Student" extends="PersonResultMap">
<result property="studentId" column="student_id"/>
</resultMap>
<resultMap id="TeacherResultMap" type="com.example.Teacher" extends="PersonResultMap">
<result property="subject" column="subject"/>
</resultMap>
解释
PersonResultMap
是基类映射,它包含所有共有的字段映射,并通过<discriminator>
根据type
字段区分不同类型的实体。<case>
标签指定了不同的类型值应该映射到哪个子类的 resultMap。例如,当type
为 ‘S’ 时,MyBatis 将使用StudentResultMap
进行进一步的映射;为 ‘T’ 时,则使用TeacherResultMap
。extends="PersonResultMap"
表示StudentResultMap
和TeacherResultMap
继承自PersonResultMap
,这样就无需在每个子类映射中重复定义公共字段。
批量操作
在 MyBatis 中实现批量插入、更新和删除操作涉及到一些特定的技巧和注意事项,具体方法如下:
批量插入
-
使用
<insert>
标签和<foreach>
标签:<insert id="batchInsert" parameterType="java.util.List"> INSERT INTO my_table (column1, column2, ...) VALUES <foreach collection="list" item="item" separator=","> (#{item.property1}, #{item.property2}, ...) </foreach> </insert>
parameterType
设置为java.util.List
或者对应的集合类型。- 使用
<foreach>
标签遍历列表,生成批量插入的 SQL。
-
注意事项:
- 数据库驱动和 MyBatis 版本需要支持 JDBC 的批处理。
- 尽量控制批量插入的数量,避免一次性插入过多数据,影响性能。
批量更新
-
使用
<update>
标签和<foreach>
标签:<update id="batchUpdate" parameterType="java.util.List"> <foreach collection="list" item="item" separator=";"> UPDATE my_table SET column1 = #{item.property1}, column2 = #{item.property2}, ... WHERE id = #{item.id} </foreach> </update>
parameterType
设置为java.util.List
或者对应的集合类型。- 使用
<foreach>
标签遍历列表,生成批量更新的 SQL。
-
注意事项:
- 更新语句需要确保每条记录都有唯一的条件(例如
WHERE id = #{item.id}
),否则会导致意外更新或执行失败。
- 更新语句需要确保每条记录都有唯一的条件(例如
批量删除
-
使用
<delete>
标签和<foreach>
标签:<delete id="batchDelete" parameterType="java.util.List"> DELETE FROM my_table WHERE id IN <foreach collection="list" item="item" open="(" close=")" separator=","> #{item.id} </foreach> </delete>
parameterType
设置为java.util.List
或者对应的集合类型。- 使用
<foreach>
标签遍历列表,生成批量删除的 SQL。
-
注意事项:
- 删除操作也需要确保每条记录有明确的删除条件,避免意外删除。
- 如果涉及到级联删除或者依赖约束,需要谨慎处理批量删除的顺序和条件。
注意事项总结:
- 参数类型:使用
java.util.List
或者对应的集合类型作为参数类型。 - SQL 生成:使用
<foreach>
标签遍历集合,生成对应的批量操作 SQL。 - 性能影响:控制每次批量操作的数量,以免影响数据库性能。
- 事务管理:在执行批量操作时,通常需要考虑事务的管理,保证操作的原子性和一致性。