han | 您所在的位置:网站首页 › 动态库调用方法有哪些 › han |
Mybatis简介
MyBatis是什么
Mybatis 是一款优秀的持久层框架,一个半 ORM(对象关系映射)框架,它支持定制化SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。Mybatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和Java 的 POJO (Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 ORM是什么ORM (Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。 为什么说MyBatis是半自动ORM映射工具?它与全自动的区别在哪里Hibernate属于全自动ORM映射工具,使用Hibernate 查询关联对象或关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis 在查询关联对象或关联集合对象时,需要手动编写SQL来完成,所以被称为半自动ORM映射工具 传统JDBC开发存在的问题 频繁创建数据库连接对象、释放,容易造成系统资源浪费,影响系统性能。可以使用连接池解决这个问题。但是使用Jdbc需要自己实现连接池。 sql语句的定义、参数设置、结果集处理存在编码。实际项目中SQL语句变化的可能性较大,一旦发生变化,需要修改java代码,系统需要重新编译,重新发布。不好维护 使用preparedStatemnet 向 占有位 符号传参数存在硬编码,因为SQL语句的where条件不一定,可能多也可能少,修改SQL还要修改代码,系统不易维护。 结果集处理存在重复代码,处理麻烦。如果可以映射成Java对象会比较方便。 JDBC编程有哪些不足之处,Mybatis是如何解决这些问题的 数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。解决方案 在Mybatis-config.xml 中配置数据库连接池,使用连接池管理数据库连接 Sql语句在写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变Java代码。解决方案: 将Sql 语句配置在XXXmapper.xml文件中与java分离 向sql语句传入参数麻烦,因为SQL语句的where不一定,可能多可能少,占位符需要和参数一一对应MMybatis自动将Java对象映射至Sql语句,通过statement中的parameterType定义输入参数的类型。 对结果集解析麻烦(查询列硬编码),Sql变化会导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成Pojo对象比较方便解决方案 MyBatis 自动将SQL执行结果映射至Java对象,通过statement 中resultType 定义输出结果类型。 MyBatis 优缺点优点: 与传统的数据库访问技术相比,ORM有以下优点: 基于SQL语句编程,相当灵活,不会对应用程序或数据库现有的设计造成影响,SQL写在XML里,接触SQL与程序代码的耦合,便于统一管理。提供XML标签,支持编写动态SQL语句,并可重用。 与JDBC相比,减少了50%的代码量,消除了JDBC代码的冗余,不需要手动开关连接 很好的与各种数据库兼容(因为MyBatis使用JDBC连接数据库,所以只要JDBC支持的数据库MyBatis都支持) 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护 能够很好的与Spring集成缺点: SQL语句的编写工作量较大,尤其是当字段多、关联表多时,对开发人员编写SQL语句得功底有一定要求 SQL语句依赖数据库,导致数据库移植性差,不能随意更换数据库 MyBatis框架适用场景 Mybatis 专注于SQL本身,是一个足够灵活的DAO层解决方案。 对性能的要求贼高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。 Hibernate 和 MyBatis 的区别相同点: 都是对Jdbc的封装,都是持久层的框架,都用于dao层的开发。 不同点 映射关系 MyBatis 是一个半自动映射框架,配置Java对象与SQL语句执行结果的对应关系,多表关联关系配置简单 Hibernate 是一个全表映射框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂 SQL优化和移植性 Hibernate 对SQL语句封装,提供了日志、缓存、级联(级联比MyBatis 强大)等特性,此外还提供 HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL语句优化困难。 MyBatis 需要手动编写SQL,支持动态SQL、处理表、动态生成表、支持存储过程。开发工作量相对大。直接使用SQL语句操作数据库,不支持数据库无关性,但SQL语句优化容易。数据库库无关性 指应用程序运行不依赖某一数据库(如SqlServer),能够简单(不再编译)切换其它数据库(如Oracle) 开发难易程度低和学习成本Hibernate 是重量级框架,学习使用门槛高,适合于需求相对稳定,中小型的项目,比如:办公自动化系统 MyBatis 是轻量级框架,学习使用门槛低,适合于需求变化频繁,大型的项目,比如:互联网电子商务系统 总结 MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架 Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架 MyBatis的解析和运行原理 MyBatis 编程步骤是怎么样的 创建SqlSessionFactory 通过SqlSessionFactory创建SqlSession 通过SqlSession执行数据库操作 调用session.commit() 提交事务 调用session.close() 关闭会话 请说说MyBatis的工作原理在学习 Mybatis 程序之前,需要了解一下 MyBatis 工作原理,以便于理解程序。Mybatis 的工作原理,如下图所示
定义 SQL 预编译指的是数据库驱动在发送 SQL 语句和参数给 DBMS 之前对SQL 语句进行编译,这样 DBMS 执行 SQL 时,就不需要重新编译。 为什么需要预编译 JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。预编译阶段可以优化 SQL 执行。预编译之后的SQL 多数情况下可以直接执行,DBMS 不需要在次编译,越复杂的SQL,编译复杂度将越大,预编译阶段可以合并多次操作为一个操作。同时预编译语句对象可以重复利用。把一个SQL 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个SQL,可以直接使用这个缓存的 PreparedStatement 对象。MyBatis 默认情况下,将对所有的 SQL 进行预编译。 Mybatis 都有哪些Executor 执行器?它们之间的区别是什么?Mybatis 有三种基本的Executor 执行器,SimpleExecutor、ReuseExecutor、BatchExecutor. SimpleExecutor:每执行一次update或select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。 ReuseExecutor:执行update或select,以SQL作为key查找Statement 对象,存在就使用,不存在就创建,用完后,不关闭Statement 对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。 BatchExecutor: 执行update (没有select,JDBC批处理不支持select),将所有SQL都添加到批处理中(addBatch()),等待统一执行(executorBatch()),它缓存了多个Statement 对象,每个Statement 对象都是addBatch()完毕后,等待逐一执行executorBatch() 批处理。与JDBC 批处理相同。作用范围 Executor 的这些特点,都严格限制在SqlSession 生命周期范围内。 MyBatis 中如何指定使用哪一种 Executor 执行器在Mybatis 配置文件中,在设置可以指定默认的 ExecutorType 执行器类型,也可以手动给DefaultSqlSessionFactory 的创建 SqlSession 的方法传递 ExecutorType 类型参数,如SqlSession中 openSession(ExecutorType execuType). 配置默认的执行器: SIMPLE 就是普通的执行器; RESUE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?Mybatis 仅支持association 关联对象和collection 关联集合对象的延迟加载。 association 指的就是一对一; collection 指的就是一对多查询。 在Mybatis 配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled = true | false 它的原理是,使用CGLIB创建目标对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器就会发现 a.getB() 是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来后,调用a.setB(b),于是a的对象b属性就有值了,接着完成 a.getB().getName()方法调用。这就是延迟加载的基本原理 当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。 映射器 #{}和${}的区别 #{}: 是占位符,预编译处理;${}: 是拼接符,字符串替换,没有预编译处理。 Mybatis 在处理#{}时,#{} 传入参数是以字符串传入,会将SQL中的#{} 替换成问号,调用 PreparedStatement 的set方法来赋值 Mybatis 在处理时,是原值传入,就是把{} 替换成变量值,相当于JDBC中的Statement编译 变量替换后,#{} 对应的变量自动加上单引号 ‘’; 变量替换后,${} 对应的变量不会加上单引号 #{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL注入 #{} 的变量替换是在 DBMS 中;${} 的变量替换是在 DBMS 外 模糊查询like语句该怎么写 “%${question}%” 可能引起SQL注入,不推荐 “%“#{question}”%” 注意:因为#{} 解析成SQL语句时候,会在变量外侧自动加单引号 ‘’,所以这里 % 需要使用双引号 “”,不能使用单引号 ‘’ ,不然会查不到任何结果。 CONCAT(‘%’,#{question}, '%')使用CONCAT() 函数(推荐) 使用bind标签 select id,sex,age,username,password from person where username LIKE #{pattern} mapper中如何传递多个参数方法1:顺序传参法 public User selectUser(String name, int deptId); select * from user where user_name = #{0} and dept_id = #{1}#{} 里面的数字代表传入参数的顺序 这种方法不建议使用,SQL层表达不直观,且一旦顺序调整容易出错 方法2:@Param 注解传参法 public User selectUser(@Param("userName") String name, int @Param("deptId") deptId); select * from user where user_name = #{userName} and dept_id=#{deptId}#{} 里面的名称对应的是注解@Param 括号里面修饰的名称 这种方法在参数不多的情况还是比较直观的,推荐使用 方法3:Map传参法 public User selectUser(Map params); // property:表示的是实体类的属性; // cloumn:表示的是数据库的字段; select * from user where user_name = #{userName} and dept_id = #{deptId}#{} 里面的名称对应的是Map里面的key名称 这种方法合适传递多个参数,且参数易变能灵活传递的情况 方法4:Java Bean传参法 public User selectUser(User user); select * from user where user_name = #{userName} and dept_id = #{deptId}#{} 里面的名称对应的是User类里面的成员属性 这种方法很直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。 MyBaits如何执行批量操作 使用foreach标签foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item,index,collection,open,separator,close。 foreach的标签属性 作用 item 表示集合中每一个元素进行迭代时的别名,随便起的变量名 index 指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用 open 表示该语句以什么开始,常用“(”; separator 表示在每次进行迭代之间以什么符号作为分隔符,常用“,”; close 表示以什么结束,常用“)”; collection 在不同情况下,属性的值是不一样的:1. 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list;2. 如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array;3. 如果传入的参数是多个的时候,我们就需要把他们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的,map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key具体做法如下 //批量保存(foreach插入多条数据两种方法) int addEmpsBatch(@Param("emps") List emps); //MySQL 下批量保存,可以使用Foreach遍历 mysql支持values(),(),()语法 INSETR INTO EMP(ename,gender,email,did) VALUES (#{emp,eName},#{emp.gender},#{emp.email},#{emp.dept.id}) // 这种方式需要数据库连接属性allowMutiQueries=true的支持 如jdbc.url=jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true INSERT INTO emp(ename,gender,email,did) VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id}) 使用ExecutorType.BATCHMyBatis内置的ExecutorType有3种,默认为simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交SQL;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能将更优;但batch模式也有自己的问题,比如在insert操作时,在事务没有提交之前,是没有办法获取到自增id,这在某型情形下是不符合业务要求的 具体用法如下 //批量保存方法测试 @Test public void testBatch() throws IOException{ SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); //可以执行批量操作的sqlSession SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH); //批量保存执行前时间 long start = System.currentTimeMills(); try{ EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.calss); for (int i=0; i < 1000; i++){ mapper.addEmp(new Employee(UUID.randomUUID().toString(),subString(0,5),"b","1")); } openSession.commit(); //批量保存时间 Long end = System.currentTimeMills(); System.out.println("执行时长" + (end - start)); //批量 预编译sql一次==》设置参数==》10000次==》执行1次 677 //非批量 (预编译=设置参数=执行 )==》10000次 1121 }finally{ openSession.close(); } }mapper和mapper.xml配置如下 public interface EmployeeMapper{ //批量保存员工 Long addEmp(Employee employee); } //批量保存员工 insert into employee(lastName,email,gender) values(#{lastName},#{email},#{gender}) 如何获取生成的主键 对于支持主键自增的数据库MySQL insert into user(user_name,user_passward,create_time) values(#{user_name},#{passWard},#{create_time,jdbcType=TIMESTAMP})parameterType 可以不写,MyBatis可以推断出传入的数据类型。如果想要访问主键,那么应当parameterType应当是java实体或者Map。这样数据在插入之后,可以通过java实体类或者Map来获取主键值。通过getUserId获取主键。 不支持主键自增的数据库(Orcale) 对于像Oracle这样的数据库,没有提供主键自增功能,而是使用序列的方式获取自增主键。 可以使用标签来获取主键的值,这种方式不仅适用于不提供主键自增功能的数据库,也适用于提供主键自增功能的数据库。 用法如下 属性 描述 keyProperty selectKey语句结果应该被设置的目标属性,如果希望得到多个生成的列,也可以是逗号分离的属性名称列表 keyColumn 匹配属性的返回结果集中的列名称。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表 resultType 结果的类型,Mybatis 通常可以推算出来。Mybatis 允许任何简单类型用作主键的类型,包括字符串。如果希望作用于多个生成的列,则可以使用一个包含期望属性的 Object或 一个Map order 值可为BEFORE或AFTER。如果是BEFORE,那么会先执行selectKey设置KeyProperty 然后执行插入语句。如果为AFTER 则相反 statementType 使用何种语句类型,默认PREPARED。有 STATEMENT,PREPARED和CALLABLE 语句的映射类型。 SELECT USER_ID.nextval as id from dual insert into user( user_id,user_name, user_password, create_time) values(#{userId},#{userName}, #{userPassword} , {createTime, jdbcType= TIMESTAMP})此时会将Orcale生成的主键赋予userId变量。这个userId 就是USER对象的属性,这样就可以将生成的主键返回了。如果仅仅是在insert语句中使用但是不返回,此时keyProperty="任意自定义变量名",resultType 可以不写。 Oracle 数据库中的值要设置为 BEFORE ,这是因为Oracle 中需要从序列获取值,然后将值注入主键插入到数据库中。 扩展 如果Mysql 使用selectKey的方式获取主键,需要注意下面两点: order:AFTER 获取递增主键值:SELECT LAST_INSERT_ID() SELECT LAST_INSERT_ID() insert into user( user_id,user_name, user_password, create_time) values(#{userId},#{userName}, #{userPassword} , {createTime, jdbcType= TIMESTAMP}) 当实体类中的属性名和表中的字段名不一样,怎么办 通过查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致 select order_id id, order_no orderno ,order_price price form orders where order_id=#{id}; 通过 来映射字段名和实体类属性名的一一对应的关系。 select * from orders where order_id=#{id} Mapper 编写有哪几种方式第一种:接口实现继承类SqlSessionDaoSupport:使用此方法需要编写mapper接口、mapper实现类、mapper.xml 文件 在sqlMapConfig.xml 中配置 mapper.xml 的位置 定义mapper接口 实现类集成SqlSessionDaoSupport mapper方法可以this.getSqlSession()进行数据增删改查 关于SqlSessionDaoSupport 如何使用 spring 配置第二种: 使用org.mybatis.spring.mapper.MapperFactoryBean: 在sqlMapConfig.xml 中配置 mapper.xml的位置,如果 mapper.xml和mapper 接口的名称相同且在同一个目录,这里可以不用配置 定义mapper接口 mapper.xml中的namespace 为 mapper 接口的地址 mapper 接口中的方法名和mapper.xml 中定义的 statement 的id保持一致 在spring配置第三种: 使用mapper扫描器 mapper.xml文件编写: mapper.xml 中的 namesapce 为 mapper 接口的地址 mapper 接口中的方法名和mapper.xml中定义的statement的id 保持一致 如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml 中配置 定义mapper接口 注意: mapper.xml 的文件名和mapper的接口名称保持一致,且放在同一目录 配置扫描 mapper 扫描器: 使用扫描器后从Spring容器中获取mapper的实现对象 什么是Mybatis的接口绑定?有哪些实现方式接口绑定,就是在Mybatis中任意定义接口,然后把接口里面的方法和SQL语句绑定,我们直接调用接口方法就可以,这样比起原来SqlSession提供的方法我们可以有更加灵活的选择和设置 接口绑定有两种实现方式 通过注解绑定,就是在接口方法上面加上@Select、@Update等注解,里面包含SQL语句来绑定; 通过XML里面的SQL来绑定,在这种情况下,要指定xml映射文件里面的namesapce必须为接口的全路径名。当Sql语句相比较简单时候,用注解绑定,当Sql语句比较复杂的时候用xml绑定,一般xml绑定的比较多。 使用MyBatis的mapper接口调用时有哪些要求? Mapper接口方法名和mapper.xml中定义的每个sql的id相同。 Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同。 Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同。 Mapper.xml文件中的那么namesapce即是mapper接口的类路径。 最佳实践中,通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个DAO接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗 介绍 Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MapperStatement的id的值,接口方法内的参数,就是传递给SQL的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement举例: com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById 的 MappedStatement。在MyBatis中,每一个、、、 标签,都会被解析为一个MappedStatement对象。 工作原理 Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为DAO接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的SQL,然后将SQL执行结果返回。 不能重载 Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。 Mybatis的XML映射文件中,不同的Xml映射文件,id是否可以重复?不同的Xml映射文件,如果配置了namesapce,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。 原因就是namespace+id是作为Map的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据相互覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。 简述Mybatis的Xml映射文件和MyBatis内部数据结构之间的映射关系?MyBatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中, 标签会被解析成ParameterMap对象,其每个子元素会被解析为ParameterMapping对象。 column 表示数据库user表中的字段card_id; property 表示User实体对象中属性card; select 表示根据column 数据库属性的值作为方法的参数; javaType 表示返回的对象类型 身份验证表XML映射文件 id,code select from card where id=#{id,jdbcType=INTEGER}一对一,一对多的案例 MyBatis是否可以映射Enum枚举类Mybatis可以映射枚举类,不单可以映射枚举类,MyBatis可以映射任何对象到表的一列上。映射方式为自定义一个TypeHandler,实现TypeHandler的setParameter()和getResult()方法。 Typehandler有两个作用 一个是完成从javaType到jdbcType的转换,二是完成JdbcType到javaType的转换,体现为setParameter()和getResult()方法,分别代表设置SQL问号占位符参数和获取列 查询结果。 动态SQL Mybatis动态sql是做什么 的?都有哪些动态SQL?能简述一下动态SQL的执行原理不?Mybatis动态SQL可以让我们在Xml映射文件内,以标签的形式编写动态SQL,完成逻辑判断和动态拼接SQL的功能,Mybatis提供了 9种动态标签trim|where|set|foreach|if|choose|when|otherwise|bind。 其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态SQL的功能。 插件模块 Mybatis是如何进行分页的?分页插件的原理是什么Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在SQL内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。 分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的SQL,然后重写SQL,根据dialect方言,添加对应的物理分页语句和物理分页参数 举例: select * from student,拦截sql后重写为:select t.* from (select * from student) t limit 0, 10 简述MyBatis的插件运行原理,以及如何编写一个插件Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。 实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住别忘了再配置文件中配置你编写的插件。 缓存 Mybatis的一级、二级缓存 一级缓存: 基于PerpetualCache 的 HashMap 本地缓存,其存储作用域为Session,当Session flush 或 close之后,该session中的所有Cache 就将清空,默认打开一级缓存。 二级缓存: 与一级缓存相同,默认也是采用PerpetualCache,HashMap 存储,不同在于其存储作用域为Mapper(Namespace),并且可自定义存储源,如Ehchche。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置\对于缓存更新机制,当某一个作用域(一级缓存Session、二级缓存Namespace)的进行了C/U/D 操作后,默认该作用域下所有 select 中缓存将被 clear。 |
CopyRight 2018-2019 实验室设备网 版权所有 |