02.MyBatis学习笔记

MyBatis简介与核心概念

Posted by Chen Xingxu on September 15, 2020

02.MyBatis学习笔记–MyBatis简介与核心概念

参考网站

https://mybatis.org/mybatis-3/zh/index.html

什么是 MyBatis

MyBatis 支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。

MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

MyBatis 可以对配置和原生 Map 使用简单的 XML 或注解,将接口和 Java 的 POJOs 映射成数据库中的记录。

XML 映射配置文件

pom.xml 文件

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.4.4</version>
</dependency>

插件(plugins)

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。

允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

使用方法

只需实现 Interceptor 接口,并指定了想要拦截的方法签名即可。

例子:

拦截在 Executor 实例中所有的 “update” 方法调用

ExamplePlugin.java

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {
  }
}

mybatis-config.xml

<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

databaseIdProvider

可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。

MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库 databaseId 属性的所有语句。

如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。 为支持多厂商特性只要像下面这样在 mybatis-config.xml 文件中加入 databaseIdProvider 即可:

<databaseIdProvider type="DB_VENDOR" />

这里的 DB_VENDOR 会通过 DatabaseMetaData#getDatabaseProductName() 返回的字符串进行设置。 由于通常情况下这个字符串都非常长而且相同产品的不同版本会返回不同的值,所以最好通过设置属性别名来使其变短,如下:

<databaseIdProvider type="DB_VENDOR">
  <property name="SQL Server" value="sqlserver"/>
  <property name="DB2" value="db2"/>        
  <property name="Oracle" value="oracle" />
</databaseIdProvider>

在有 properties 时,DB_VENDOR databaseIdProvider 的将被设置为第一个能匹配数据库产品名称的属性键对应的值,如果没有匹配的属性将会设置为 “null”。 在这个例子中,如果 getDatabaseProductName() 返回“Oracle (DataDirect)”,databaseId 将被设置为 “oracle”。

你可以通过实现接口 org.apache.ibatis.mapping.DatabaseIdProvider 并在 mybatis-config.xml 中注册来构建自己的 DatabaseIdProvider:

public interface DatabaseIdProvider {
  void setProperties(Properties p);
  String getDatabaseId(DataSource dataSource) throws SQLException;
}

映射器(mappers)

概念

用于引用定义好的映射定义,告诉 MyBatis 去哪里找我们的 SQL 定义配置。

四种方式

  1. 直接引用 xml 文件

    <mappers>  
         <mapper resource="mybatis/UserMapper.xml" />  
    </mappers>
    
  2. 通过绝对路径引用,注意在绝对路径前加上:“file:///”

    <mappers>  
         <mapper url="file:///D:/mybatisLearn/yangxu-mybatis-demo/yangxu-mybatis-demo-01/src/main/resources/mybatis/UserMapper.xml"/> 
    </mappers>
    
  3. 引用 mapper 接口对象的方式

    <mappers>  
         <mapper class="demo.yangxu.mybatis.mapper.UserMapper"/>
    </mappers>
    
  4. 引用 mapper 接口包的方式

    <mappers> 
    	<!--通过字节方式去读取,这种方式有乱码问题-->
    	<package name="demo.yangxu.mybatis.mapper"/>
    </mappers>
    

配置环境(environments)

概念

配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中。

不过要记住:尽管可以配置多个环境,每个 SqlSessionFactory 实例只能选择其一。

使用场景

  1. 为了开发设置不同的数据库配置;
  2. 测试和生产环境数据库不同;
  3. 有多个数据库却共享相同的模式,即对不同的数据库使用相同的 SQL 映射。

代码

<properties resource="config.properties"></properties>
<!-- 对事务的管理和连接池的配置 -->  
<!-- 用default指定默认的数据库链接:<environments default="oracle_jdbc">-->
<environments default="oracle_jdbc">  
    <environment id="oracle_jdbc">  
        <transactionManager type="JDBC" />  
        <dataSource type="POOLED">  
            <property name="driver" value="${jdbc.oracle.driverClassName}" />  
            <property name="url" value="${jdbc.oracle.url}" />  
            <property name="username" value="${jdbc.oracle.username}" />  
            <property name="password" value="${jdbc.oracle.password}" />   
        </dataSource>  
    </environment>  

    <environment id="mysql_jdbc">  
        <transactionManager type="JDBC" />  
        <dataSource type="POOLED">  
            <property name="driver" value="${jdbc.mysql.driverClassName}" />  
            <property name="url" value="${jdbc.mysql.url}" />  
            <property name="username" value="${jdbc.mysql.username}" />  
            <property name="password" value="${jdbc.mysql.password}" />   
        </dataSource>  
    </environment> 
</environments>

根据数据库环境,获取 SqlSessionFactory

SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment);
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment,properties);

核心概念

mybatis-config.xml

XML 配置文件(configuration XML)中包含了对 MyBatis 系统的核心设置,包含获取数据库连接实例的数据源(DataSource)和决定事务作用域和控制方式的事务管理器(TransactionManager)。

示例

<?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 resource="config.properties" />
	
	<!-- 别名 -->
	<typeAliases>
        <typeAlias type="demo.yangxu.mybatis.pojo.User" alias="user"></typeAlias>
    </typeAliases>

	<!-- 环境配置 -->
	<environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>

	<!-- 映射文件 -->
	<mappers>
         <mapper resource="mybatis/UserMapper.xml"/>
    </mappers>

</configuration>

SqlSessionFactoryBuilder

概念

SqlSessionFactoryBuilder 通过类名就可以看出这个类的主要作用就是创建一个 SqlSessionFactory,通过输入 MyBatis 配置文件的字节流或者字符流,生成 XMLConfigBuilder,XMLConfigBuilder 创建一个 Configuration,Configuration 这个类中包含了 MyBatis 的配置的一切信息,MyBatis 进行的所有操作都需要根据 Configuration 中的信息来进行。

作用域(Scope)和生命周期

可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但是最好还是不要让其一直存在,以保证所有的 XML 解析资源开放给更重要的事情。

这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。

代码

package org.apache.ibatis.session;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Properties;

import org.apache.ibatis.builder.xml.XMLConfigBuilder;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;

/**
 * Builds {@link SqlSession} instances.
 *
 * @author Clinton Begin
 */
public class SqlSessionFactoryBuilder {

  public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
  }

  public SqlSessionFactory build(Reader reader, String environment) {
    return build(reader, environment, null);
  }

  public SqlSessionFactory build(Reader reader, Properties properties) {
    return build(reader, null, properties);
  }

  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

  public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment) {
    return build(inputStream, environment, null);
  }

  public SqlSessionFactory build(InputStream inputStream, Properties properties) {
    return build(inputStream, null, properties);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
    
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

}

SqlSessionFactory 接口

概念

SQL 会话工厂,用于创建 SqlSession。

作用域(Scope)和生命周期

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由对它进行清除或重建。

最佳作用域是应用作用域。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

如何创建

使用 xml 构建

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

Java 代码构建

DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

代码

package org.apache.ibatis.session;

import java.sql.Connection;

/**
 * Creates an {@link SqlSession} out of a connection or a DataSource
 * 
 * @author Clinton Begin
 */
public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

SqlSession 接口

概念

SqlSession 是 MyBatis 的一个重要接口,定义了数据库的增删改查以及事务管理的常用方法。

SqlSession 还提供了查找 Mapper 接口的有关方法。

作用域(Scope)和生命周期

每个线程都应该有它自己的 SqlSession 实例。

SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。

每次收到的 HTTP 请求,就可以打开一个 SqlSession,返回一个响应,就关闭它。

如何创建

SqlSession session = sqlSessionFactory.openSession();
try {
  // do work
} finally {
  session.close();
}

代码

package org.apache.ibatis.session;

import java.io.Closeable;
import java.sql.Connection;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.BatchResult;

/**
 * The primary Java interface for working with MyBatis.
 * Through this interface you can execute commands, get mappers and manage transactions.
 *
 * @author Clinton Begin
 */
public interface SqlSession extends Closeable {

  /**
   * Retrieve a single row mapped from the statement key
   * @param <T> the returned object type
   * @param statement
   * @return Mapped object
   */
  <T> T selectOne(String statement);

  /**
   * Retrieve a single row mapped from the statement key and parameter.
   * @param <T> the returned object type
   * @param statement Unique identifier matching the statement to use.
   * @param parameter A parameter object to pass to the statement.
   * @return Mapped object
   */
  <T> T selectOne(String statement, Object parameter);

  /**
   * Retrieve a list of mapped objects from the statement key and parameter.
   * @param <E> the returned list element type
   * @param statement Unique identifier matching the statement to use.
   * @return List of mapped object
   */
  <E> List<E> selectList(String statement);

  /**
   * Retrieve a list of mapped objects from the statement key and parameter.
   * @param <E> the returned list element type
   * @param statement Unique identifier matching the statement to use.
   * @param parameter A parameter object to pass to the statement.
   * @return List of mapped object
   */
  <E> List<E> selectList(String statement, Object parameter);

  /**
   * Retrieve a list of mapped objects from the statement key and parameter,
   * within the specified row bounds.
   * @param <E> the returned list element type
   * @param statement Unique identifier matching the statement to use.
   * @param parameter A parameter object to pass to the statement.
   * @param rowBounds  Bounds to limit object retrieval
   * @return List of mapped object
   */
  <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);

  /**
   * The selectMap is a special case in that it is designed to convert a list
   * of results into a Map based on one of the properties in the resulting
   * objects.
   * Eg. Return a of Map[Integer,Author] for selectMap("selectAuthors","id")
   * @param <K> the returned Map keys type
   * @param <V> the returned Map values type
   * @param statement Unique identifier matching the statement to use.
   * @param mapKey The property to use as key for each value in the list.
   * @return Map containing key pair data.
   */
  <K, V> Map<K, V> selectMap(String statement, String mapKey);

  /**
   * The selectMap is a special case in that it is designed to convert a list
   * of results into a Map based on one of the properties in the resulting
   * objects.
   * @param <K> the returned Map keys type
   * @param <V> the returned Map values type
   * @param statement Unique identifier matching the statement to use.
   * @param parameter A parameter object to pass to the statement.
   * @param mapKey The property to use as key for each value in the list.
   * @return Map containing key pair data.
   */
  <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);

  /**
   * The selectMap is a special case in that it is designed to convert a list
   * of results into a Map based on one of the properties in the resulting
   * objects.
   * @param <K> the returned Map keys type
   * @param <V> the returned Map values type
   * @param statement Unique identifier matching the statement to use.
   * @param parameter A parameter object to pass to the statement.
   * @param mapKey The property to use as key for each value in the list.
   * @param rowBounds  Bounds to limit object retrieval
   * @return Map containing key pair data.
   */
  <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);

  /**
   * A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.
   * @param <T> the returned cursor element type.
   * @param statement Unique identifier matching the statement to use.
   * @return Cursor of mapped objects
   */
  <T> Cursor<T> selectCursor(String statement);

  /**
   * A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.
   * @param <T> the returned cursor element type.
   * @param statement Unique identifier matching the statement to use.
   * @param parameter A parameter object to pass to the statement.
   * @return Cursor of mapped objects
   */
  <T> Cursor<T> selectCursor(String statement, Object parameter);

  /**
   * A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.
   * @param <T> the returned cursor element type.
   * @param statement Unique identifier matching the statement to use.
   * @param parameter A parameter object to pass to the statement.
   * @param rowBounds  Bounds to limit object retrieval
   * @return Cursor of mapped objects
   */
  <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);

  /**
   * Retrieve a single row mapped from the statement key and parameter
   * using a {@code ResultHandler}.
   * @param statement Unique identifier matching the statement to use.
   * @param parameter A parameter object to pass to the statement.
   * @param handler ResultHandler that will handle each retrieved row
   */
  void select(String statement, Object parameter, ResultHandler handler);

  /**
   * Retrieve a single row mapped from the statement
   * using a {@code ResultHandler}.
   * @param statement Unique identifier matching the statement to use.
   * @param handler ResultHandler that will handle each retrieved row
   */
  void select(String statement, ResultHandler handler);

  /**
   * Retrieve a single row mapped from the statement key and parameter
   * using a {@code ResultHandler} and {@code RowBounds}
   * @param statement Unique identifier matching the statement to use.
   * @param rowBounds RowBound instance to limit the query results
   * @param handler ResultHandler that will handle each retrieved row
   */
  void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);

  /**
   * Execute an insert statement.
   * @param statement Unique identifier matching the statement to execute.
   * @return int The number of rows affected by the insert.
   */
  int insert(String statement);

  /**
   * Execute an insert statement with the given parameter object. Any generated
   * autoincrement values or selectKey entries will modify the given parameter
   * object properties. Only the number of rows affected will be returned.
   * @param statement Unique identifier matching the statement to execute.
   * @param parameter A parameter object to pass to the statement.
   * @return int The number of rows affected by the insert.
   */
  int insert(String statement, Object parameter);

  /**
   * Execute an update statement. The number of rows affected will be returned.
   * @param statement Unique identifier matching the statement to execute.
   * @return int The number of rows affected by the update.
   */
  int update(String statement);

  /**
   * Execute an update statement. The number of rows affected will be returned.
   * @param statement Unique identifier matching the statement to execute.
   * @param parameter A parameter object to pass to the statement.
   * @return int The number of rows affected by the update.
   */
  int update(String statement, Object parameter);

  /**
   * Execute a delete statement. The number of rows affected will be returned.
   * @param statement Unique identifier matching the statement to execute.
   * @return int The number of rows affected by the delete.
   */
  int delete(String statement);

  /**
   * Execute a delete statement. The number of rows affected will be returned.
   * @param statement Unique identifier matching the statement to execute.
   * @param parameter A parameter object to pass to the statement.
   * @return int The number of rows affected by the delete.
   */
  int delete(String statement, Object parameter);

  /**
   * Flushes batch statements and commits database connection.
   * Note that database connection will not be committed if no updates/deletes/inserts were called.
   * To force the commit call {@link SqlSession#commit(boolean)}
   */
  void commit();

  /**
   * Flushes batch statements and commits database connection.
   * @param force forces connection commit
   */
  void commit(boolean force);

  /**
   * Discards pending batch statements and rolls database connection back.
   * Note that database connection will not be rolled back if no updates/deletes/inserts were called.
   * To force the rollback call {@link SqlSession#rollback(boolean)}
   */
  void rollback();

  /**
   * Discards pending batch statements and rolls database connection back.
   * Note that database connection will not be rolled back if no updates/deletes/inserts were called.
   * @param force forces connection rollback
   */
  void rollback(boolean force);

  /**
   * Flushes batch statements.
   * @return BatchResult list of updated records
   * @since 3.0.6
   */
  List<BatchResult> flushStatements();

  /**
   * Closes the session
   */
  @Override
  void close();

  /**
   * Clears local session cache
   */
  void clearCache();

  /**
   * Retrieves current configuration
   * @return Configuration
   */
  Configuration getConfiguration();

  /**
   * Retrieves a mapper.
   * @param <T> the mapper type
   * @param type Mapper interface class
   * @return a mapper bound to this SqlSession
   */
  <T> T getMapper(Class<T> type);

  /**
   * Retrieves inner database connection
   * @return Connection
   */
  Connection getConnection();
}

Mapper 接口

概念

承载了实际的业务逻辑,其生命周期比较短,由 SqlSession 创建,用于将 Java 对象和实际的 SQL 语句对应起来。

Mapper 接口是指程序员自行定义的一个数据操纵接口,类似于通常所说的 DAO 接口。跟 DAO 不同的地方在于 Mapper 接口只需要程序员定义,不需要程序员去实现,MyBatis 会自动为 Mapper 接口创建动态代理对象。Mapper 接口的方法通常与 Mapper 配置文件中的 select、insert、update、delete 等 XML 结点存在一一对应关系。

实现方式

  1. 使用 XML 配置文件的方式;
  2. 使用注解方式;
  3. 直接使用 MyBatis 提供的 API。

#{} 和 ${} 的区别

#{}

解读

# 方式能够很大程度防止 SQL 注入。

使用 #{} 格式的语法在 MyBatis 中使用 Preparement 语句来安全的设置值。

PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1,id);

例子

执行 SQL:

Select * from emp where name = #{employeeName}

参数:employeeName 传入值为:Smith

解析后执行的 SQL:

Select * from emp where name = 

${}

解读

有时想直接在 SQL 语句中插入一个不改变的字符串,比如 ORDER BY, $ 将传入的数据直接显示生成在 SQL 中。

Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);

例子

执行SQL:

Select * from emp where name = ${employeeName}

参数:employeeName 传入值为:Smith

解析后执行的SQL:

Select * from emp where name =Smith

总结

# 方式能够很大程度防止 SQL 注入,$ 方式无法防止 SQL 注入;

$ 方式一般用于传入数据库对象;

使用 $ 要么不允许用户输入这些字段,要么自行转义并检验;

一般能用 # 的就别用 $

MyBatis 的优缺点

优点

  1. 易于上手和掌握;

  2. SQL 写在 xml 里,便于统一管理和优化;

  3. 解除 SQL 与程序代码的耦合;

  4. 提供映射标签,支持对象与数据库的 ORM 字段关系映射;

  5. 提供对象关系映射标签,支持对象关系组建维护;

  6. 提供 xml 标签,支持编写动态 SQL。

缺点

  1. SQL 工作量很大,尤其是字段多、关联表多时,更是如此;

  2. SQL 依赖于数据库,导致数据库移植性差;

  3. 由于 xml 里标签 id 必须唯一,导致 DAO 中方法不支持方法重载;

  4. 字段映射标签和对象关系映射标签仅仅是对映射关系的描述,具体实现仍然依赖于 SQL。(比如配置了一对多 Collection 标签,如果 SQL 里没有 join 子表或查询子表的话,查询后返回的对象是不具备对象关系的,即 Collection 的对象为 null);

  5. DAO 层过于简单,对象组装的工作量较大;

  6. 不支持级联更新、级联删除;

  7. 编写动态 SQL 时,不方便调试,尤其逻辑复杂时;

  8. 提供的写动态 SQL 的 xml 标签功能简单(连 Struts 都比不上),编写动态 SQL 仍然受限,且可读性低;

  9. 若不查询主键字段,容易造成查询出的对象有“覆盖”现象;

  10. 参数的数据类型支持不完善。(如参数为 Date 类型时,容易报没有 get、set 方法,需在参数上加 @param);

  11. 多参数时,使用不方便,功能不够强大。(目前支持的方法有 map、对象、注解 @param 以及默认采用 012索引位的方式);

  12. 缓存使用不当,容易产生脏数据。