MyBatis详解

文章目录

一. 动态代理

1.动态代理

代理:应该自己做的事情,却请了别人来做,被请的人就是代理对象
动态代理:在程序运行过程中产生的这个对象
而程序运行过程中产生对象其实就是刚才反射,所以,动态代理其实就是通过反射来生成一个代理.
在java 中 proxy类和一个InvocationHandler接口,生成一个动态代理对象 jdk提供的代理只能针对接口做代理,我们有更强大的代理cglib (框架)
在这里插入图片描述
InvocationHandler 是代理实例的调用处理程序 实现的接口
在这里插入图片描述

2.具体实现

getClass():取得当前对象所属的Class对象   
getClassLoader():取得该Class对象的类装载器

在这里插入图片描述
在这里插入图片描述
使用动态代理必须有类方法的接口

  1. 先产生一个handle对象
  2. 然后将想要的类对象的类加载器 handle对象 和类下面的接口的放入Proxy.newProxyInstance中 就会返回一个你想要的对象

二. mybatis 框架

主要有:

  • 对原生态jdbc程序(单独使用jdbc开发)问题总结
  • Mybatis框架原理
  • Mybatis润程序
    用户的增,删,改,查
  • Mybatis开发dao两种方法
  • 原始dao开发方法(程序需要编写dao接口和dao实现类)
  • Mybaits和mapper接口(相当于dao接口)代理开发方法
  • Mybatis配置文件 SqlMapConfig.xml
  • Mybatis核心
    Mybatis输入映射
    Mybatis输出映射

1.原生态的jdbc的问题

使用jdbc查询sqlsever数据库中的用户表
Sql_table 记录表结构
Sql_data.sql测试数据
问题:

  1. 数据库连接
    Connection connention= null;
    预编译的Statement ,使用预编译的Statement提高数据库性能
    PreparedSatement pre=null;
    ResultSet resultSet=null;
    数据库中的?代表占位符
    数据库的链接 使用时创建 不使用的时候就释放 对数据库进行频繁的开启和关闭 造成对数据库资源的浪费
    解决方案 通过连接池对数据进行管理
  2. 将sql语句硬编码到java代码中,如果sql语句修改,需要重新编译java代码,不利于系统维护,设想,将sql语句配置在xml文件中,即使sql变化,也不需要对java代码进行重新编译
  3. 向preparedStatement中设置参数,对占位符位置和设置参数值,硬编码在java代码 中,不利于系统维护
  4. 在resultSet中遍历结果集数据时,存在硬编码,将获取表的字段进行硬编码== 不利于系统维护==
    设想: 将查询的结果集自动映射成java对象

2. Mybais 框架

Mybatis是什么呢?
Mybatis是一个持久层框架,是apach提供的映射方式,自由灵活生成(半自动化,大部分需要程序元编写sql)满足需求sql语句
mybatis托管到goolecode下,再后来托管到github下(https://github.com/mybatis/mybatis-3/releases)。
Mybatis可以将我们向preparedStatement中的输入的参数
自动进行输入映射,将查询结果集灵活映射成java对象(输出映射)

3. MyBatis框架图示

在这里插入图片描述

4. 入门程序

4.1需求:

根据用户id(主键)查询用户信息
根据用户名称模糊查询用户信息
添加信息
删除信息
更新用户

4.2环境

Java环境 jdk 1.7
Exlipse
Mysql
MyBatis运行环境
在这里插入图片描述
Lib下:依赖包
Mybatis 3.2.7jar 核心包
log4j-1.2.17.jar:日志包
在这里插入图片描述
cglib-2.2.2.jar动态代理:
在这里插入图片描述
加入mysql的驱动包

4.3 log4j.properties 是日志包 查看运行过程

在classpath下创建log4j.properties如下

# Global logging configuration
log4j.rootLogger=DEBUG, stdout
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

在这里插入图片描述

4.4 工程结构

在这里插入图片描述

4.5 sqlMapConfig.xml

配置:mybatis的运行环境 数据源 事务
在这里插入图片描述

4.6 根据用户的id 来查询用户的信息

4.6.1. 写好一个po类 准备放select的查询结果

在这里插入图片描述

4.6.2.映射文件()

映射文件的命名
User.xml(原始ibatis命名) mapper代理开发映射文件名称叫xxxMapper.xml 比如:UserMapper.xml、ItemsMapper.xml
在映射文件中配置sql语句

  • 输入映射 paramenterType=”int” 规定输入类型为int
  • 输出映射 resultType=” “cn.mybatis.po.User””为po类的路径
    在这里插入图片描述

4.6.3.在sqlMapConfig.xml加载映射文件

在这里插入图片描述

4.6.4.写实体类

在这里插入图片描述

①	.得到配置文件流
  String resource="SqlMapConfig.xml";
		
		//得到配置文件流
		InputStream inputStream= Resources.getResourceAsStream(resource);
②	.创建绘画工厂传入mybatis配置文件
    SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);.通过工厂得到sqlsession 
   SqlSession sqlSession=sqlSessionFactory.openSession();.通过sqlsession操作数据库
User user=sqlSession.selectOne("test.findUserByid",1);.第一个参数是命名空间+.+ID
⑥	得到了破类对象

4.7 利用用户名称模糊查询用户信息

4.7.1 映射文件

使用user.xml添加利用用户名称模糊查询用户信息语句
在这里插入图片描述
resultType 指定的是单条记录的Java对象类型

 ${}  表示 拼接sql串  将接受的参数内容不加任何修饰的拼接在sql中
 使用${} 拼接sql   引起sql注入  

${value} 就收输入参数内容 如果传入的参数是简单类型,$${ }中只能用value

4.7.2 . 测试程序

在这里插入图片描述

List<User> list=sqlSession.selectList("test.findUserByName", "小明");
	for (User user : list) {
		System.out.println(user);
		sqlSession.close();
	} 

4.8 总结:

4.8.1 parameterType

在映射文件中通过parameterType 指定输入参数类型

4.8.2 resultType

 在映射文件中通过resultType指定输出结果的类型

4.8.3 #{} ${}

#{} 代表占位符 ${} 代表字符串的拼接 无任何添加的拼接 会引起sql注入 所以不议使用

4.8.4 selectone() 和selectList()

selectone() 查询出一条记录进行映射 如果使用seleone 也可以使用selectList
selectList() 表示查询出一个列表(多条记录) 进行映射 如果使用selectList 不可以使用seleone
在这里插入图片描述

4.9 添加用户

4.9.1 映射文件

在user .xml中配置添加用户的statement
在这里插入图片描述

<!-- 这是添加用户的sql语句 
parameterTyper  指定输入参数类型  参数类型是pojo(包括用户信息)
#{} 中指定pojo的属性名 接受pojo对象的属性值,mybatis通过OGNL获取对象的属性值-->
<select id="adduserinfor" parameterType="cn.mybatis.po.User" >
insert into user(username,birthday,sex,address) value (#{username},#{birthday},#{sex},#{address})
</select>

因为在sql中设置id为自增的所以配置文件不用写sql中的id

4.9.2 程序代码

在这里插入图片描述

public  void  adduserinfor(){                                                                          
	String resource="SqlMapConfig.xml";                                                                
	 try {                                                                                              
			InputStream inputStream=Resources.getResourceAsStream(resource);                               
			SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);         
			SqlSession sqlSession=sqlSessionFactory.openSession();                                         
			User user=new User();                                                                          
			user.setUsername("田家坤");                                                                       
			user.setBirthday(new Date());                                                                  
			user.setSex("1");                                                                              
			user.setAddress("韩城");                                                                         
			List<User> list=sqlSession.selectList("test.adduserinfor",user );                              
			System.out.println(list);                                                                      
			                                                                                               
			sqlSession.commit();                                                                           
			sqlSession.close();                                                                            
					                                                                                       
			                                                                                               
		} catch (IOException e) {                                                                          
			// TODO Auto-generated catch block                                                             
			e.printStackTrace();       

4.9.3 自增主键的返回

Mysql自增主键 执行insert提交之前自动生成一个自增主键
通过一个mysql函数获取刚提交的掺入记录的自增主键
Last_insert_ID() 获取的id 是0
是insert之后调用此函数
在这里插入图片描述

  • 程序中的代码
    在这里插入图片描述
@Test
	public  void  adduserinfor(){
		String resource="SqlMapConfig.xml";
		try {
			InputStream inputStream=Resources.getResourceAsStream(resource);
			SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
			SqlSession sqlSession=sqlSessionFactory.openSession();
			User user=new User();
			user.setUsername("坤");
			user.setBirthday(new Date());
			user.setSex("3");
			user.setAddress("韩-城");
			int list=sqlSession.insert("test.adduserinfor",user );
			//System.out.println(list);
			
			sqlSession.commit();
	      //获取用户信息的主键
			System.out.println("user"+user);
			sqlSession.close();

4.9.4 非自增主键 主键返回 (使用UUID())

使用mysql的uuid() 函数生成主键 需要修改表中的id字段为string类型 长度设置为35
执行思路:

  • 现将uuid() 查询到主键,将逐渐输入到sql语句中
  • 执行uuid()语句顺序相对于insert语句是之前
<!-- 先执行uuid()  将uuid查到的属性值赋给keyporperty 中的id
 然后insert执行的时候是  先把user中的属性加给insert的id
注意  得把user表中的id属性变成string类型的  
这样才会获取到
 -->
 <selectKey keyProperty="id" order="BEFORE" resultType="String">
 select uuid()
 </selectKey>
 insert into user(id,username,birthday,sex,address) value (#{id},#{username},#{birthday},#{sex},#{address})

 </insert>

在这里插入图片描述
oracle的无自动添加id的 获取方法
在这里插入图片描述

4.10 删除用户:

A.映射文件
在这里插入图片描述

<!-- 删除用户需要输入用户的id -->
 <delete id="deleteUser" parameterType="int">
 delete from user where id=#{id}
 </delete>

B.测试程序
在这里插入图片描述

4.11 更新用户

A.映射文件
在这里插入图片描述

<!-- parameterType=""指定的是user对象  包括id  但是id必须存在 
 #{id}  从输入user对象中获取id值
 -->
 <update id="updateUserInfor" parameterType="cn.mybatis.po.User">
 upadte user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address}  where id=#{id} 
 </update>

B.测试程序

@Test
public void upadteUserInfor() throws IOException{
   String resource="SqlMapConfig.xml";
   
   InputStream inputStream=Resources.getResourceAsStream(resource);
   SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
   SqlSession sqlSession=sqlSessionFactory.openSession();
   User user=new User();
   user.setAddress("陕西省");
   user.setBirthday(new Date());
   user.setId(39);
   user.setSex("1");
   user.setUsername("xiaoming ");
   
   sqlSession.update("test.updateUserInfor", user);
   sqlSession.commit();
   sqlSession.close();

三. Mybatis开发Dao的方法

1 SqlSession使用范围

1.1、 SqlSessionFactoryBuilder()

通过SqlSessionFactoryBuilder()创建会话工厂
将SqlSessionFactory ,当成一个工具类使用即可,不需要使用单例管理SqlSessionFactoryBuilder(),在需要创建SqlSessionFactory时候,只需new一次SqlSessionFactoryBuilder()即可

1.2. SqlSessionFactory

通过SqlSessionFactory创建SqlSession,使用单例模式管理SqlSessionFactory(工厂一旦创建,使用一个实例)
将来mybatis和spring整合后,使用单例 模式管理sqlSessionFactory

1.3. SqlSession

SqlSession是一个面向用户(程序员)的接口
Sqlsession中提供很多操作数据库的方法
SqlSession是线程不安全的,在sqlsession实现 类中除了有接口的方法(操作数据库的方法)还有数据域的属性
SqlSession最佳应用场合在方法体内,定义成局部变量

2. 原始Dao开发方法(程序员需要写Dao接口和Dao实现类)

2.1. 思路:

程序员需要写dao接口和dao实现类
需要向dao实现类中注入sqlseessionFactory,在方法 体内通过SqlSessionFactory 创建sqlsession

2.2. Dao接口

在这里插入图片描述

2.3. Dao接口实现类

在这里插入图片描述

2.4. 测试类

在这里插入图片描述

2.5. 原始dao的方法的问题

  1. Dao接口实现类方法中存在着大量模板方法,设想 能否将这些代码提取出来,大大减轻程序员到的工作量
  2. 调用sqlsession方法时 将statement的id硬编码了
  3. 调用sqlsession 方法时将传入的变量,由于sqlsession方法使用泛型,及时变量类型传错,在编一阶段也不会报错,不利于开发

四.Mapper代理方法(程序员只需写mapper的接口 相当与dao的接口 )

程序员还需编写mapper.xml映射文件

程序员只需要写mapper接口需要遵循一些开发规范(相当于dao接口)
Mabatis自动将生成mapper接口实现类的代理对象

1 开发规范

A. namespace等于mapper接口的地址
在这里插入图片描述
B. mapper.Java接口的方法名和mapper.xml的id一致
C. mapper接口中方法的输入参数类型和mapper.xml中statement的parameterType指定的类型一致
D. mapper接口中的方法返回值类型和mapper.xml中statement的resultType指定的类型一致

在这里插入图片描述
在这里插入图片描述
总结:以上的开发规范 主要是对下面代码进行统一生成
在这里插入图片描述

2 mapper.java

在这里插入图片描述

3 mapper.xml

在这里插入图片描述

4 测试代码

在这里插入图片描述

五. sqlMapConfig.xml

1. SqlMapConfig.xml中配置的内容和顺序如下:

properties(属性)
settings(全局配置参数)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境集合属性对象)
environment(环境子属性对象)
transactionManager(事务管理)
dataSource(数据源)
mappers(映射器)

1.1 properties(属性)

需求:将数据库的链接参数配置在db.properties中,只需要在SqlMapConfig.xml中加载db.properties的属性值
在SqlMapConfig.xml中就不需要在数据库的连接参数不需要进行硬编码了
将数据库的连接参数配置在db.properties中,方便程序员进行批量操作
Db.properties
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200213200316825.png

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
jdbc.username=root
jdbc.password=root
注意:url后面的一系列的东西  有可能造成输出结果为0

在SqlMapConfig.xml配置Db.properties
在这里插入图片描述
在这里插入图片描述
注意: MyBatis 将按照下面的顺序来加载属性:
 在 properties 元素体内定义的属性首先被读取。
 然后会读取properties 元素中resource或 url 加载的属性,它会覆盖已读取的同名属性。
最后读取parameterType传递的属性,它会覆盖已读取的同名属性。
建议不要在properties中不要添加任何属性值,只将属性值定义在properties文件中
在properties文件中定义属性名要有一定的特殊性如xxxx.xxxx.xxx
因此,通过parameterType传递的属性具有最高优先级,resource或 url 加载的属性次之,最低优先级的是 properties 元素体内定义的属性。

2.settings(全局配置参数)

mybatis全局配置参数,全局参数将会影响mybatis的运行行为。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.TypeAilases(别名) 重点

3.1需求:

mapper.xml中定义了很多statement,statement需要parameterType指定输入参数类型,需要resultType指定输出结果的映射类型
如果在指定类型时输入类型全路径不方便开发,可以设置定义针对parameterType、resultType的别名
Mybatis默认支持别名:

别名映射的类型
_bytebyte
_longlong
_shortshort
_intint
_integerint
_doubledouble
_floatfloat
_booleanboolean
stringString
byteByte
longLong
shortShort
intInteger
integerInteger
doubleDouble
floatFloat
booleanBoolean
dateDate
decimalBigDecimal
bigdecimalBigDecimal

3.2自定义别名 po类

3.2.1第一种:

定义别名:
在这里插入图片描述
引用别名:
在这里插入图片描述

3.2.2第二种:
批量定义别名:
<typeAliases>
<!-- 
针对单个别名定义
type  类型的路径
alias 别名
 -->
<!-- <typeAlias type="cn.mybatis.po.User" alias="user"/> -->
<!-- 批量自动定义别名 
指定包名:mybatis自动扫描包中的po类,自动定义别名,别名就是类名,(首字母大写和小写都可以)
-->
<package name="cn.mybatis.po"/>
</typeAliases>

在这里插入图片描述

3.2.3. 第三种:

定义好多包

4. typeHandlers(类型处理器)

Mybatis中通过typeHandlers(类型处理器)完成jdbc类型和Java类型的转换
一般不需要自定义
Mybatis自己的足够用
类型处理器用于java类型和jdbc类型映射,如下:

<select id="findUserById" parameterType="int" resultType="user">
		select * from user where id = #{id}
</select>

mybatis自带的类型处理器基本上满足日常需求,不需要单独定义。

mybatis支持类型处理器:

类型处理器Java类型JDBC类型
BooleanTypeHandlerBoolean,boolean任何兼容的布尔值
ByteTypeHandlerByte,byte任何兼容的数字或字节类型
ShortTypeHandlerShort,short任何兼容的数字或短整型
IntegerTypeHandlerInteger,int任何兼容的数字和整型
LongTypeHandlerLong,long任何兼容的数字或长整型
FloatTypeHandlerFloat,float任何兼容的数字或单精度浮点型
DoubleTypeHandlerDouble,double任何兼容的数字或双精度浮点型
BigDecimalTypeHandlerBigDecimal任何兼容的数字或十进制小数类型
StringTypeHandlerStringCHAR和VARCHAR类型
ClobTypeHandlerStringCLOB和LONGVARCHAR类型
NStringTypeHandlerStringNVARCHAR和NCHAR类型
NClobTypeHandlerStringNCLOB类型
ByteArrayTypeHandlerbyte[]任何兼容的字节流类型
BlobTypeHandlerbyte[]BLOB和LONGVARBINARY类型
DateTypeHandlerDate(java.util)TIMESTAMP类型
DateOnlyTypeHandlerDate(java.util)DATE类型
TimeOnlyTypeHandlerDate(java.util)TIME类型
SqlTimestampTypeHandlerTimestamp(java.sql)TIMESTAMP类型
SqlDateTypeHandlerDate(java.sql)DATE类型
SqlTimeTypeHandlerTime(java.sql)TIME类型
ObjectTypeHandler任意其他或未指定类型
EnumTypeHandlerEnumeration类型VARCHAR-任何兼容的字符串类型,作为代码存储(而不是索引)。

5. mappers(映射器)

5.1加载单个映射文件

在这里插入图片描述

<!--  加載映射文件 -->
   <mappers>
   <!-- 通过resource方法  一次映射一个xml文件 -->
        <mapper resource="mapper/UserMapper.xml"/>
   </mappers>

5.2 <mapper url=" " />

使用完全限定路径
如:<mapper url="file:///D:\workspace_spingmvc\mybatis_01\config\sqlmap\User.xml" />

5.3 通过mapper接口来进行加载

<mapper class=" " />
遵循规范:需要将mapper接口类名和mapper.xml映射文件 名称保持一致,且在一个目录中,上边规范的前提是:使用的是mapper代理方法
在这里插入图片描述
在这里插入图片描述

5.4 mapper接口的包名 推荐使用

批量加载mapper
指定mapper接口的包名,mybatis自动扫描报销面的所有mapper接口进行加载
遵循规范:需将mapper接口类名和mapper.xml映射文件名称保持一致,且在一个目录下,使用mapper代理方法
在这里插入图片描述
在这里插入图片描述

6. 输入映射

6.1 parameterType(输入类型)

#{}与${}

#{}实现的是向prepareStatement中的预处理语句中设置参数值,sql语句中#{}表示一个占位符即?。

<!-- 根据id查询用户信息 -->
	<select id="findUserById" parameterType="int" resultType="user">
		select * from user where id = #{id}
	</select>

使用占位符#{}可以有效防止sql注入,在使用时不需要关心参数值的类型mybatis会自动进行java类型和jdbc类型的转换。
#{}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称。

${}和#{}不同,通过${}可以将parameterType 传入的内容拼接在sql中且不进行jdbc类型转换,${}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,${}括号中只能是value。使用${}不能防止sql注入,但是有时用${}会非常方便,如下的例子:

<!-- 根据名称模糊查询用户信息 -->
	<select id="selectUserByName" parameterType="string" resultType="user">
	   select * from user where username like '%${value}%'
	</select>

如果本例子使用#{}则传入的字符串中必须有%号,而%是人为拼接在参数中,显然有点麻烦,如果采用${}在sql中拼接为%的方式则在调用mapper接口传递参数就方便很多。

//如果使用占位符号则必须人为在传参数中加%
List list = userMapper.selectUserByName("%管理员%");

//如果使用${}原始符号则不用人为在参数中加%
Listlist = userMapper.selectUserByName(“管理员”);

再比如order by排序,如果将列名通过参数传入sql,根据传的列名进行排序,应该写为:

ORDER BY ${columnName}

如果使用#{}将无法实现此功能。

6.2 传递简单类型

参考上边的例子。

6.3 传递pojo对象

Mybatis使用ognl表达式解析对象字段的值,如下例子:

<!—传递pojo对象综合查询用户信息 -->

	<select id="findUserByUser" parameterType="user" resultType="user">
	   select * from user where id=#{id} and username like '%${username}%'
	</select>

上边红色标注的是user对象中的字段名称。

  • 测试:
Public void testFindUserByUser()throws Exception{
		//获取session
		SqlSession session = sqlSessionFactory.openSession();
		//获限mapper接口实例
		UserMapper userMapper = session.getMapper(UserMapper.class);
		//构造查询条件user对象
		User user = new User();
		user.setId(1);
		user.setUsername("管理员");
		//传递user对象查询用户列表
		List<User>list = userMapper.findUserByUser(user);
		//关闭session
		session.close();
	}

  • 异常测试:
    Sql中字段名输入错误后测试,username输入dusername测试结果报错
org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'dusername' in 'class cn.itcast.mybatis.po.User'
### Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'dusername' in 'class cn.itcast.mybatis.po.User'

6.4 传递pojo的包装对象

6.4.1. 需求

完成用户信息的综合查询,需要传入查询条件(可能包含用户信息,其他信息等)
针对上面的需求,建议使用自定义的包装类型的pojo
在pojo的包装类型的pojo中将复杂的查询提哦啊见包装进去
在这里插入图片描述
用户的包装类型 根据自己需求进行编写
在这里插入图片描述

6.4.2 mapper.xml中

在usermapper.xml中定义用户信息综合查询(查询条件复杂,通过高级查询进行复杂关联查询)
在这里插入图片描述
输入的参数属性应该是 该类的属性 不需用 类名.属性
只有类的属性是另一个类 才需要要类(属性).属性
控制台中出现了乱码的原因: 是mybatis扫描了多余的东西 使用了别名

7. 输出映射

7.1 resultType

使用resultType进行输出映射,只有查询出来的列名称pojo中的属性一致,该列才可以映射成功
如果查询出来的列名和pojo的属性全部不一致,没有创建pojo对象
只要查询出来的列明和pojo中的属性有一个一致,就会创建pojo对象

7.1.1输出简单类型

需求:
用户信息的综合查询的列表总数,通过查询总数和上边用户综合查询列表才可以实现分页

7.1.1.1 mapper.xml

在这里插入图片描述

<!--                                                                                                         
 用户信息综合查询                                                                                                     
 parameterType  指定输入类型 
resultType的类型为简单类型                                                                                        
  -->                                                                                                         
 <select id="findUserCount" parameterType="cn.mybatis.po.UserQueryVo" resultType="int">                       
 select count(*) from user where user.sex=#{userCustom.sex} and user.username like '%${userCustom.username}%' 
 </select>        
7.1.1.2 mapper.Java

在这里插入图片描述
如果查出来的数据为一行或者一列则可以将输出类型定义为简单类型
总结:A. 不管输出的pojo的那个对象还是一个列表(list中包括pojo)在mapper.xml中resultType指定的类型是一样的,在mapper.Java指定的方法返回值类型不一样

  1. 输出单个pojo对象,方法返回值是单个对象类型
  2. 输出pojo兑现list,方法返回值是list
    生成的动态代理对象是根据mapper方法的返回值类型确定是调用selectone还是selectList

7.2 resultMap

如果查询出来的列名和pojo的属性名不一致,通过定义一个resultMap列名和pojo属性名之间做一个映射关系

7.2.1 定义resultMap

在这里插入图片描述

<!--
resultMap   type 是指的第映射的po类型
id  表示  resultMap的唯一表示


<id>表示 查询结果集中的唯一标识
column="_id"  代表主键的列 名     property  代表映射为typepo类的具体属性

 -->
<resultMap type="user" id="userResultMap">
<id column="_id" property="id"/>
<result  column="_username" property="username"/>

</resultMap>
7.2.2 使用resultMap作为statement

在这里插入图片描述

8. 动态sql

Mybatis核心对sql语句进行灵活操作,通过表达式是进行判断,对sql进行拼接和组装

8.1 需求:

用户信息综合查询列表和用户信息查询列表总数这两个statement的定义使用动态sql

<select id="findUserList"  parameterType="cn.mybatis.po.UserQueryVo" resultType="cn.mybatis.po.UserCustom">
select *from user
<where>
<if test="userCustom!=null and userCustom!=''">
 <if test="userCustom.sex!=null and userCustom.sex!='' ">
and user.sex=#{userCustom.sex}
 </if>
 <if test="userCustom.username!=null and userCustom.username!=''">
and user.username like '%${userCustom.username}%' 
</if>
</if>
</where>
<!--  where user.sex=#{userCustom.sex} and user.username like '%${userCustom.username}%' -->
</select>	

在这里插入图片描述
用where标签的话sql语句会自动屏蔽第一and
这样很方便

8.2 测试代码

在这里插入图片描述

9.Sql片段

9.1. 需求

将上面实现动态sql判断代码块抽取出来,组成一个sql片段,其他的statement sql片段

9.2定义sql片段

在这里插入图片描述

<!-- 定义sql片段
id sql片段的唯一标识

经验:  sql片段是基于单标来定义的
这样这个sql片段的可重用度高
sql片段中不要包括where 
 -->
 <sql id="query_user_where">

<if test="userCustom!=null and userCustom!=''">
 <if test="userCustom.sex!=null and userCustom.sex!='' ">
 </if>
 and user.sex=#{userCustom.sex}
 <if test="userCustom.username!=null and userCustom.username!=''"></if>
 and user.username like '%${userCustom.username}%' 
</if>
 </sql>

9.3 引用sql片段

在这里插入图片描述

10. foreach

向sql传递数组或list mybatis使用foreach解析

10.1 需求:

在用户查询列表和查询总数的statement中增加多个id输入查询
Sql语句如下:
Select *from user where id=1 or id=10 or id=16
Select *from user where id in{1,10,16}

10.1.1再输入参数类型中添加List ids传入多个id

在这里插入图片描述

10.2 修改mapper.xml

在这里插入图片描述

10.3测试代码

在这里插入图片描述
在这里插入图片描述

六 Mybatis 的高级映射

  • 课程复习:
  1. Mybatis是什么?
    Mybatis是一个持久层框架,myatis是一个不完全的ORM框架,sql语句需要程序员自己编写,但是mybatis也有映射(输入参数映射,输出结果的参数
    Myatis入门门槛不高,学习成本低,让程序员吧精力放到sql语句上,对sql语句优化非常方便,适用于需求变化较多的项目,比如互联网项目
  2. Mybatis框架执行过程:
    1. 匹配mybatis的配置文件,sqlMapConfig.xml(名称不固定
    2. 通过配置文件,加载mybatis运行环境,创建sqlSessionFactory在实际使用时是单例模式
    3. 通过sqlSessionFactory创建Sqlsession sqlSession是一个面向用户接口(提供操作数据库方法,实际对象是线程不安全,建议sqlSession应用场合在方法体内)
    4. 调用sqlSession的方法去操作数据
    如果需要提交事务没需要执行SqlSession的commit()方法
    5. 资源释放 关闭sqlsession
    Mybatis开发dao方法
    1. 原始的dao方法
      需要程序员编写dao接口和实现类
      需要在dao实现类中注入一个sqlSessionFactory工厂
    2. Mapper代理开发方法
      只需程序员编写mapper接口(就是dao接口)
      程序员在编写mapper.xml(映射文件) 和mapper.Java需要 遵循一个开发路径
      1. Mapper.xml中的namespace就是mapper.Java的类路径完全一致
      2. Mapper.xml中statement的id和mapper.java中的方法名一致
      3. Mapper.xml中的statement的parameterTyp指定输入参数的类型和mapper.Java的方法输入参数类型一致
      4. Mapper.xml中statement的resultType指定输出结果的类型和 mapper.Java的方法返回值类型一致
        SqlMapConfig.xml配置文件:可以配置propertes属性 别名 mapper加载。。。
  • 输入映射:
    ParamaterType:指定输入参数类型可以是简单类型 pojo hashmap
    对于综合查询,建议 parameterType使用包装的pojo,有利于系统的扩展
  • 输出映射:
    ResultType:
    查询到的列名和resultType指定的pojo属性名一致才能映射成功
    Resulmap:
    可以通过resultmap完成一些高级映射
    如果查询到列名和饮食和的pojo的属性名不一致时,通过resultMap设置列名和属性名之间的对应关系(映射关系)可以完成映射
  • == 高级映射:==
    1. 将关联查询的列表映射到pojo属性中(一对一)
    2. 将关联查询的列映射到一个List(一对多)
  • 动态sql:(重点)
    If判断 (判断)
    Where
    Foreach
    Sql片段(掌握)
    课程安排:
    对订单商品的数据模型进行分析
    高级映射:
    实现一对一查询 一对多 多对多
    延迟加载
    查询缓存
    一级缓存
    二级缓存(了解mybatis二级缓存使用场景)‘’
    Mybatis和spring的整合
    逆向工程(会用)

1. 订单商品数据模型

在这里插入图片描述

1.1 数据模型分析思路:

  1. 每张表的数据内部
    分模块对每张表疾苦内容进行熟悉,相当于你学习系统需求(功能)的过程
  2. 每张表重要的字段设置
    分清楚单张表的主键额外键字段和非空的
  3. 数据库级别表与表的之间关系
    外键关系
  4. 表与表之间的业务关系
    在分析表与表之间的业务关系时要建立在业务爷爷基础上的分析

1.2模型数据分析

在这里插入图片描述

  • 用户表user:
    记录了购买商品的用户信息

  • 订单表:orders
    记录了用户所创建的订单(购买商品的订单)

  • 订单明细表:orderdetail:
    记录了订单的详细信息即购买商品的信息

  • 商品表:items
    记录了商品信息

表与表之间的业务关系:
在分析表与表之间的业务关系时需要建立 在某个业务意义基础上去分析。
先分析数据级别之间有关系的表之间的业务关系:

usre和orders:
user---->orders:一个用户可以创建多个订单,一对多
orders—>user:一个订单只由一个用户创建,一对一

orders和orderdetail:
orders—》orderdetail:一个订单可以包括 多个订单明细,因为一个订单可以购买多个商品,每个商品的购买信息在orderdetail记录,一对多关系

orderdetail–> orders:一个订单明细只能包括在一个订单中,一对一

orderdetail和itesm:
orderdetail—》itesms:一个订单明细只对应一个商品信息,一对一

items–> orderdetail:一个商品可以包括在多个订单明细 ,一对多

再分析数据库级别没有关系的表之间是否有业务关系:
orders和items:
orders和items之间可以通过orderdetail表建立 关系。

2.一对一查询:

2.1 需求

查询订单信息,关联查询创建订单的用户信息

2.2 resultType

2.2.1 sql语句

确定查询主表,订单表
确定查询关联表:用户表
关联查询使用内链接还是外链接
由于orders表中有一个外键(user_id) 通过外键关联查询用户表只能查询出一条记录,可以使用内链接

SELECT 
 orders.*,
 USER.username,
 USER.sex,
 USER.address 
FROM
 orders,
 USER 
WHERE orders.user_id = user.id
2.2.1创建pojo

将上边sql查询的结果映射到pojo中,pojo中必须包括所有查询的列名
原始的orders.java不能映射全部字段,需要创建的pojo
创建一个pojo继承包括查询字段较多的po类
在这里插入图片描述

2.2.2 xml

在这里插入图片描述

2.2.3测试类

在这里插入图片描述

2.2 resulMap

2.2.1 sql语句

同resultType的sql文件相同

2.2.2 使用resultMap映射思路

使用resultMap将查询的结果映射到Orders对象()中,在orders类中添加user属性,将关联查询出的用户信息映射到orders对象的user属性中

2.3.3需要在orders类中添加 user属性

在这里插入图片描述

2.2.3 Mapper.xml

在这里插入图片描述
在这里插入图片描述

2.3.5 Mapper.java
2.3.5.1 定义接口

在这里插入图片描述

2.3.5.2 测试程序

在这里插入图片描述

2.4 resultType 和resultMap的区别(实现一对一查询的小结)

实现一对一
使用resultType实现较为简单,如果 pojp中没有包括查询出来的列明,需要增加类名对象的属性,即可完成映射
如果没有查询结果的特殊要求建议使用resultType
ResultMap需要单独定义resultMap 实现有点马鞍,如果有对查询结果有特殊2

3一对多

3.1 需求:

查询订单及顶大明细信息

3.2 sql语句

确定主查询表 订单表
确定关联查询表 订单明细表
在一对一查询的基础上添加订单明细表的关联即可

3.3 java代码

3.3.1 在orders中添加list订单明细属性

在这里插入图片描述

3.4 mapper.xml

3.4.1 select语句的定义

在这里插入图片描述

3.4.2 resultMap的定义

在这里插入图片描述

<!-- 查询订单关联用户查询及 订单明细 -->
	<resultMap type="cn.mybatis.po.Orders" id="findOrdersAndOrderDetailResultMap"
		extends="findUserResultMap">
		<!--用继承就不用在重新配置订单信息和用户信息的配置文件了  -->
		<!-- 订单信息 -->

		<!-- 订单明细 -->
		<!--订单关联查询出了多条明细,要使用collection进行映射 -->
		<!--ofType映射pojo类中集合路径 -->
		<collection property="orderdetails" ofType="cn.mybatis.po.Orderdetail">
			<!-- id代表集合属性的唯一表示 -->
			
			<id column="orderdetail_id" property="id" />
			<result column="orders_id" property="ordersId" />
			<result column="items_id" property="itemsId" />
			<result column="items_num" property="itemsNum" />

		</collection>
	</resultMap>

3.5 mapper.java

在这里插入图片描述

3.6 测试类

在这里插入图片描述

/*查询订单关联用户查询及 订单明细 用resultMap*/                                                                   
@Test                                                                                            
public void testFindOrdersAndOrderDetailResultMap() throws Exception{                            
     SqlSession sqlSession=sqlSessionFactory.openSession();                                       
   OrdersMapperCustom  ordersMapperCustom= sqlSession.getMapper(OrdersMapperCustom.class);      
   List<Orders> orders= ordersMapperCustom.findOrdersAndOrderDetailResultMap();                 
   System.out.println(orders);                                                                  
}        

3.7 分析
使用resultType将上边的查询结果映射到pojo中,订单信息的就会重复

在这里插入图片描述
要求:
对orders映射下不能出现重复记录

在orders.java类中添加List orderDetail睡醒
最终将会在订单信息 映射到orders中,订单所对象的订单明细映射到orders中的orderDatils属性中
在这里插入图片描述
最终映射成orders记录数为两条 orders信息不重复,
每个orders中的orderDatil属性存储了该订单的订单明细
因为配置文件中的唯一标识 所以将重复项去除

3.8 小结:

Mybaits使用collection对关联查询的多条记录映射到一个list集合属性中
使用resultType 实现
将订单明细映射到orders中的orderDatils中,需要自己处理将重复项去除

4 多对多:

4.1需求

查询用户及用户购买商品 的信息

4.2 分析

查询主表是 :用户表
关联表:由于用户信息和商品没有直接关联,通过订单和订单明细进行关联,所以关联表 orders , orderDatil, itme

4.3 sql语句

select
		orders.*,
		user.username,
		user.sex,
		user.address,
		orderdetail.id
		orderdetail_id,
		orderdetail.items_id,
		orderdetail.items_num,
		orderdetail.orders_id,
    items.name items_name ,
    items.price  items_price,
    items.detail items_detail
		from
		user,
		orders,
		orderdetail,
                                items
		where
		user.id=orders.user_id and orderdetail.orders_id=orders.id and
                              orderdetail.items_id=items.id    

4.4 映射思路:

将用户信息映射到user中
在user类中添加订单列表属性list orderlist将用户创建爱你的订单映射到orderlist
在orders 中添加了订单明细列表属性list orderdetail 将订单的明细映射到orderDetail中
在orderDetail中添加items属性,将订单明细所对应的映射到items

4.5 mapper.xml

在这里插入图片描述

4.6 resultMap

在这里插入图片描述

4.7 mapper.java

在这里插入图片描述

4.8 多对多的查询总结;

将查询用户购买的商品 信息明细清单(用户名 用户地址 购买商品名称 购买商品时间 够爱商品数量)
针对上边的需求就是用resultType件查询的记录映射到一个扩展的pojo中很简单实现明细清单的功能
使用resultMap是针对对查询结果应设有特殊要求的功能比如list中包含多个lit
要是没有就可以用resultType

5. resultMap的总结:

           resultType:
  • 作用:
    查询结果按照sql列名pojo属性名一致性映射到pojo中。

  • 场合:
    常见一些明细记录的展示,比如用户购买商品明细,将关联查询信息全部展示在页面时,此时可直接使用resultType将每一条记录映射到pojo中,在前端页面遍历list(list中是pojo)即可。

  • resultMap:
    使用association和collection完成一对一和一对多高级映射(对结果有特殊的映射要求)

  • association:
    作用:
    将关联查询信息映射到一个pojo对象中。
    场合:
    为了方便查询关联信息可以使用association将关联订单信息映射为用户对象的pojo属性中,比如:查询订单及关联用户信息。

    使用resultType无法将查询结果映射到pojo对象的pojo属性中,根据对结果集查询遍历的需要选择使用resultType还是resultMap。

  • collection:
    作用:
    将关联查询信息映射到一个list集合中。
    场合:
    为了方便查询遍历关联信息可以使用collection将关联信息映射到list集合中,比如:查询用户权限范围模块及模块下的菜单,可使用collection将模块映射到模块list中,将菜单列表映射到模块对象的菜单list属性中,这样的作的目的也是方便对查询结果集进行遍历查询。
    如果使用resultType无法将查询结果映射到list集合中。

七. 延迟加载

1. 什么是延迟加载

 ResultMap可以实现高级映射(使用association collection 实现一对一额一对多的映射 其具备延迟加载的功能)
需求:
 如果查询订单并且关联查询用户信息,如果先查询订单信息即可满足要求,当我们需要查询用户信息时在查询用户信息,吧对用户信息的按需去查询就是延迟加载
延迟加载:先从单标查询,需要时再从关联标曲关联查询,大大提高数据库的性能,因为查询单标要比关联查询多表速度要快

2. 使用association实现延迟加载

2.1需求:

查询订单并且关联查询用户信息

2.2 Mapper.xml

需要定义两个mapper的方法对象的statement

  1. 只查询订单信息
    select *from orders;
    在查询订单的statement中使用association去延迟加载(执行)下边的statement关联查询用户信息
    在这里插入图片描述
    2关联查询用户信息
    通过上边查询到的订单信息中user_id去关联查询用户信息
    使用finduserByid
    在这里插入图片描述
    上边先去执行findOrderUserLazlyLoading 当需要去查询用户信息的时候子啊去执行findUserById 通过resultMap的定义将将延至加载执行配置起来

2.3延迟加载resultMap

使用association中的select 指定延迟加载去执行stataement的id

  <!-- 对用户信息进行延迟加载 -->
	<!-- select:指定延迟加载需要执行的statement的id
			(是根据user_id查询用户信息的statement)
			 column:订单信息中关联用户信息查询的列:user_id
			 要使用userMapper,
			 关联查询寻的sql语句为:
			 select orders.*,
           (select username from user where orders.user_id=user.id) username,
           (select sex from user where orders.user_id=user.id) sex
            from orders
			 -->
		<association property="user" javaType="cn.mybatis.po.User" select="findUserByid1" column="user_id">
<!-- 实现用户信息的延迟加载 -->
			
		</association>
	</resultMap>
	<!-- 查找用户的信息的statement -->
   <select id="findUserByid1"  parameterType="int" resultType="cn.mybatis.po.User" >
       select *from user where id=#{id}<!-- id表示输入的参数是id -->
   </select>
	<!-- 查询订单关联查询用户,用户信息需要延迟加载 -->
	<select id="findOrderUserLazyLoading" resultMap="OrderUserLazyLoadingResultMap">
	select *from orders
	</select>

2.4 mapper.java

在这里插入图片描述

2.5 测试程序

2.5.1 测试思路:

执行上边的mapper方法(findorderUserLazyLoading),内部调用 findOrderUserLazyLoading只查询orders信息(单表) 获取一个list

在程序中去遍历list《Orders》,当我们调用orders中的getUser方法时,开始进行延迟加载
延迟加载去调findUserById

2.5.2延迟加载配置

Mybatis默认没有开始延迟加载,需要在sqlMapConfig.xml中配置setting
在这里插入图片描述
在sqlConfig.xml配置参数

<!-- 打开延迟加载的开关-->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 架构积极加载的开关关闭-->
<setting name="aggressivelazyLoading" value="false"/>
</settings>
2.5.3测试代码:

在这里插入图片描述

2.5.4 延迟加载的思考:

不用mybatis提供的association和collection中的延迟加载功能,如何实现延迟加载??
实现方法如下:
定义两个mapper方法

  1. 查寻订单列表
  2. 根据用户id查询用户信息
    实现思路:先去查询第一个mapper方法,获取订单信息列表
    在程序中sevice 按需去调用第二个mapper方法去查询用户信息
    使用延迟加载方法,先去查询寻简单的sql(最好用sql 也可以关联查询)

3.什么是查询缓存?

3.1 Mybatis提供查询缓存 用于减轻数据压力,提高数据库的性能

Mybatis提供一级缓存 和 二级缓存
在这里插入图片描述
在操作数据库时
一级缓存是 sqlsession级别的缓存 在操作数据库时需要构造sqlsession对象,在对象中有一个数据结构hashmap用于存储缓存数据,不同的sqlsession之间的缓存数据的区域(hashmap)是互相不影响的
二级缓存是mapper级别的缓存,多个sqlsession去操作同一个mapper的sql语句,duogesqlsession可以共用二级缓存,,二级缓存是跨sqlsession的

为什么要用缓存:
如果缓存中有数据就不用从数据库中去获取,大大提高系统的性能

3.2一级缓存

3.2.1 一级缓存工作原理

在这里插入图片描述
第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1 的用户信息,如果有,从数据库查询用户信息
得到用户信息将用户信息 存储到一级缓存中

如果sqlsession去执行commit 操作(如执行插入更新) 清空sqlsession中的一级缓存,这样做的目的是为了 让缓存中存储的是最新的信息,避免脏读

第二次发起查询用户id为1用户信息,先去找 缓存中是否有id为1的用户信息,缓存中油,直接 从缓存中获取用户信息

3.2.2 一级缓存的测试:

在这里插入图片描述
在这里插入图片描述
一及缓存的应用
正常使用的时候,试讲mybatis和spring进行整合开发 事务处理是在service中,一个service方法包括很多mapper方法调用
Service
//开始执行时,开始事务,创建sqlsession
//第一次调用mapper的finduserById()

//第二次调用mapper方法finduser Byid

//方法结束 sqlsession关闭
如果是执行两次service调用查询时的用户信息,不走一级缓存 因为service方法结束 sqlsession就关闭 一级缓存就会情景

3.3 二级缓存

3.3.1 二级缓存的原理

在这里插入图片描述
首先开启mybatis的二级缓存
SqlSession去查询用户id为1的用户 查询到用户信息会将查询数据存储的二级缓存中
如果说sqlsession3去执行相同的mapper下的sql 执行commit提交,清空该mapper下的二级缓存区域的数据

Sqlsession2去查询用户为id 为1的用户信息,去缓存中找是否存在数据,如果存在直接从缓存中取出数据
二级缓存与一级缓存其实是差不多的
区别: 二级缓存比一级缓存的范围大 多个sqlsession可以共享一个usermapper二级缓存的区域
Usermapper有一个二级缓存区域 其他的mapper也有自己的缓存区域
Usermapper的缓存区域是按namespace分的
每一个namespace有一个mapper的二级缓存区域
两个mapper的namespqce如果相同 这两个mapper执行的sql查询的数据缓存将有一个相同的二级缓存

3.3.2开启二级缓存

Mybatis的二级缓存是mapper范围级别,除了在SqlMapConfig.xml设置二级缓存的总开关,还要再具体的mapper.xml中开启
在这里插入图片描述
SqlMapConfig.xml开启二级缓存:
在这里插入图片描述
在userMapper.xml开启二级缓存,usermapper 下的sql执行完成会存储到他们的缓存区域(hashMap)
在这里插入图片描述

3.3.3 调用pojo类实现序列化接口

在这里插入图片描述
为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多张多样,不一定在内存

3.3.4 测试方法

在这里插入图片描述

3.3.5 useCache配置

在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓存。

<select id="findOrderListResultMap" resultMap="ordersUserMap" useCache="false">

总结:针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存。

3.3.6 刷新缓存(就是清空缓存)

在mapper的同一个namespace中,如果有其它insert、update、delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。

设置statement配置中的flushCache=“true” 属性,默认情况下为true即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
如下:

总结:一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。
4.二级应用场景
对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度,业务场景比如:耗时较高的统计分析sql、电话账单查询sql等。
实现方法如下:通过设置刷新间隔时间,由mybatis每隔一段时间自动清空缓存,根据数据变化频率设置缓存刷新间隔flushInterval,比如设置为30分钟、60分钟、24小时等,根据需求而定。

  1. 二级缓存局限性
    mybatis二级缓存对细粒度的数据级别的缓存实现不好比如如下需求:对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息,因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。

八.mybatis整合ehcache

ehcache是一个分布式缓存框架。

1. 分布缓存

我们系统为了提高系统并发,性能、一般对系统进行分布式部署(集群部署方式)

在这里插入图片描述
不使用分布缓存,缓存的数据在各各服务单独存储,不方便系统 开发。所以要使用分布式缓存对缓存数据进行集中管理。

mybatis无法实现分布式缓存,需要和其它分布式缓存框架进行整合。

  1. 整合方法(掌握)

mybatis提供了一个cache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。

2.1 mybatis和ehcache整合,mybatis和ehcache整合包中提供了一个cache接口的实现类。
在这里插入图片描述

2.2 mybatis默认实现cache类是:

在这里插入图片描述

3.加入ehcache包

在这里插入图片描述

3.1整合ehcache

配置mapper中cache中的type为ehcache对cache接口的实现类型。
在这里插入图片描述

3.2加入ehcache的配置文件

在classpath下配置ehcache.xml
在这里插入图片描述

最后是逆向工程 我写在外面

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值