深入MyBatis开发之mybatis映射器

       映射器的配置元素有select/insert/update/delete/sql/resultMap/cache/cache-ref八个常用的,parameterMap基本不用,也不建议使用。

    MyBatis3官方文档:http://www.mybatis.org/mybatis-3/zh/configuration.html

    本blog实例代码:https://github.com/JeeLearner/learning-ssmr   chapter05和chapter05.2

一、select元素

在工作中用的最多的是id,parameterType、resultType、resultMap。如果要设置缓存,还会使用到flushCache、useCache。其它的都不常用。

1.自动映射和驼峰映射

在mybatis-config.xml中settings元素中,autoMapping代表自动映射,默认值PARTIAL;mapUnderscoreToCamelCase代表驼峰映射,默认值false。

2.传递多个参数

(1)使用map传递参数。可读性差,维护困难,摒弃。

(2)@Param注解传递多个参数。参数小于等于5个,可使用。比用JavaBean更好。

(3)参数大于5个时,使用JavaBean方式。

(4)混合使用。

public interface RoleDao {
    public List<Role> findRolesByMap(Map<String, Object> parameterMap);
    public List<Role> findRolesByAnnotation(@Param("roleName") String rolename, @Param("note") String note);
    public List<Role> findRolesByBean(RoleParams roleParam);
    public List<Role> findByMix(@Param("params") RoleParams roleParams, @Param("page") PageParams pageParams);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ssmr.chapter05.dao.RoleDao">
	<select id="findRolesByMap" parameterType="map" resultType="role">
		select id, role_name as roleName, note 
		from t_role
		where role_name like concat('%', #{roleName}, '%')
			and note like concat('%', #{note}, '%')
	</select>

	<select id="findRolesByAnnotation" resultType="role">
		select id,role_name as roleName, note 
		from t_role
		where role_name like concat('%', #{roleName}, '%')
			and note like concat('%', #{note}, '%')
	</select>

	<select id="findRolesByBean" parameterType="com.ssm.chapter5.param.RoleParams"
		resultType="role">
		select id, role_name as roleName, note 
		from t_role
		where role_name like concat('%', #{roleName}, '%')
			and note like concat('%',#{note}, '%')
	</select>

	<select id="findByMix" resultType="role">
		select id, role_name as roleName, note 
		from t_role
		where role_name like concat('%',#{params.roleName}, '%')
			and note like concat('%', #{params.note}, '%')
		limit #{page.start}, #{page.limit}
	</select>
</mapper>
3.使用resultMap映射结果集

id是标识,type代表哪个类作为其映射的类,可以是别名或全限定名。

子元素id代表resultMap的主键,而result代表其属性。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ssmr.chapter05.dao.RoleDao">

	<resultMap id="roleMap" type="role">
		<id property="id" column="id" />
		<result property="roleName" column="role_name" />
		<result property="note" column="note" />
	</resultMap>

	<select id="getRoleUseResultMap" parameterType="long" resultMap="roleMap">
		select id, role_name, note 
		from t_role 
		where id = #{id}
	</select>
</mapper>
4.分页参数RowBounds

只需给接口增加一个RowBounds参数即可。

public interface RoleDao {
    public List<Role> findRolesByRowBounds(@Param("roleName") String rolename, @Param("note") String note, RowBounds rowBounds);
}
<select id="findByRowBounds" resultType="role">
	select id, role_name as roleName, note
	from t_role
	where role_name like concat('%',#{params.roleName}, '%')
		and note like concat('%', #{params.note}, '%')
	limit #{page.start}, #{page.limit}
</select>
测试:

public class Main {

    public static void main(String[] args) {
        testRowBounds();
    }

    public static void testRowBounds(){
        SqlSession sqlSession = null;
        try {
            Logger log = Logger.getLogger(Main.class);
            sqlSession = SqlSessionFactoryUtil.openSqlSession();
            RoleDao roleDao = sqlSession.getMapper(RoleDao.class);
            //offset属性是偏移量,即从第几行开始读取记录
            //limit是限制条数
            RowBounds rowBounds = new RowBounds(0, 20);
            List<Role> roleList = roleDao.findRolesByRowBounds("role_name", "note", rowBounds);
            System.out.println(roleList.size());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(sqlSession != null){
                sqlSession.close();
            }
        }
    }
}


二、insert元素

<insert id="insertRole" parameterType="role">
	insert into t_role(role_name, note) 
	values(#{roleName},#{note})
</insert>
主键回填:

useGeneratedKeys:获取数据库生成的主键,默认false。代表采用JDBC的Statement对象的getGeneratedKeys方法返回主键。

keyProperty代表将用哪个POJO的属性去匹配这个主键。

<insert id="insertRole" parameterType="role" useGeneratedKeys="true" keyProperty="id">
	insert into t_role(role_name, note)
	values(#{roleName},#{note})
</insert>
自定义主键:

规则:当角色表记录为空时id设置为1;不为空时,id设置为当前id加3。

keyProperty指定了采用哪个属性作为POJO的主键

resultType代表将返回一个long类型的结果集

order设置为BEFORE说明它将于当前定义的SQL前执行。

order设置为AFTER说明它会在插入语句之后执行。比如一些插入语句内部可能有嵌入索引调用。

<insert id="insertRole" parameterType="role">
	<selectKey keyProperty="id" resultType="long" order="BEFORE">
		select if (max(id) = null, 1, max(id) + 3)
		from t_role
	</selectKey>
	insert into t_role2(id, role_name, note) 
	values(#{id}, #{roleName},#{note})
</insert>


三、update元素和delete元素

<update id="updateRole" parameterType="role">
	update t_role set role_name = #{roleName}, note = #{note}
	where id = #{id}
</update>

<delete id="deleteRole" parameterType="long">
	delete from t_role where id = #{id}
</delete>


四、sql元素

sql元素的作用在于可以定义一条SQL的一部分。

1.基本用法:

<resultMap id="roleMap" type="role">
	<id property="id" column="id" />
	<result property="roleName" column="role_name" />
	<result property="note" column="note" />
</resultMap>

<sql id="roleCols">
	id, role_name, note
</sql>
<select id="getRoleUseResultMap" parameterType="long" resultMap="roleMap">
	select <include refid="roleCols"/>
	from t_role
	where id = #{id}
</select>
2.支持变量传递:

在include元素中定义了一个命名为alias的变量,其值是SQL中表t_role的别名r。然后sql元素就可以使用这个变量名了。

<sql id="roleCols">
	${alias}.id, ${alias}.role_name, ${alias}.note
</sql>
<select id="getRoleUseResultMap" parameterType="long" resultMap="roleMap">
	select 
		<include refid="roleCols">
			<property name="alias" value="r"/>
		</include>
	from t_role r
	where id = #{id}
</select>


五、参数

1.概述

<insert id="insertRole" parameterType="role" useGeneratedKeys="true" keyProperty="id">
	insert into t_role(role_name, note)
	values(#{roleName, typeHandler=org.apache.ibatis.type.StringTypeHandler},#{note})
</insert>
事实上mybatis会根据javaType和jdbcType自动检测使用哪个typeHandler。但是我们可以自定义typeHandler。

#{age, javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
mybatis也提供了一些对控制数值的精度支持。如下只保留两位有效数字。

#{width, javaType=double,jdbcType=NUMERIC,numericScale=2}
2.存储过程参数支持

参数类型:输入参数、输出参数、输入输出参数

#{id, mode=IN}
#{roleName, mode=OUT}
#{note, mode=INOUT}

六、resultMap元素

1.resultMap元素构成

<resultMap>
	<constructor>
		<idArg/>
		<arg/>
	</constructor>
	<id/>
	<result/>
	<association/>
	<collection/>
	<discriminator>
		< case/>
	</discriminator>
</resultMap>
constructor元素用于配置构造方法。一个POJO可能不存在没有参数的构造方法,可以使用constructor进行配置。假设Role不存在没有参数的构造方法,它的构造方法声明为public Role(Integer id, String roleName)。那么需要配置结果集,如下:

<resultMap ......>
	<constructor>
		<idArg column="id" javaType="int"/>
		<arg column="role_name" javaType="string"/>
	</constructor>
	......
</resultMap>
2.使用map存储结果集

一般而言,任何select语句都可以使用map存储:

<select id="findColorByNote" parameterType="string" resultType="map">
	select id, color, note
	from t_color
	where note like concat('%', #{note}, '%')
</select>
但是map可读性差,更多时候会使用POJO方式。

3.使用POJO存储结果集

resultMap元素的子元素id表示这个对象的主键,property代表着POJO的属性名称。

<resultMap id="roleMap" type="role">
	<id property="id" column="id"/>
	<result property="roleName" column="role_name"/>
	<result property="note" column="note"/>
</resultMap>

<select id="getRoleUseResultMap" parameterType="long" resultMap="roleMap">
	select id, role_name, note
	from t_role
	where id = #{id}
</select>

七、级联

1.概述

    association一对一级联

    collection一对多级联

    discriminator鉴别器级联

2.简单DEMO示例

    级联实例完整版DEMO级联DEMO

public class Employee {
	private Long id;
	private String realName;
	//涉及鉴别器级联
	private SexEnum sex = null;
	private Date birthday;
	private String mobile;
	private String email;
	private String position;
	private String note;
        //工牌按一对一级联
	private WorkCard workCard;
	//雇员任务,一对多级联
	private List<EmployeeTask> employeeTaskList = null;
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.ssmr.chapter05.dao.EmployeeDao">
	<resultMap id="employeeMap" type="com.ssmr.chapter05.pojo.Employee">
		<id column="id" property="id" />
		<result column="real_name" property="realName" />
		<result column="sex" property="sex"
				typeHandler="com.ssmr.chapter05.typehandler.SexTypeHandler" />
		<result column="birthday" property="birthday" />
		<result column="mobile" property="mobile" />
		<result column="email" property="email" />
		<result column="position" property="position" />
		<result column="note" property="note" />
		<!--雇员工牌表一对一级联-->
		<association property="workCard" column="id"
					 select="com.ssmr.chapter05.dao.EmployeeTaskDao.getEmployeeTaskByEmpId"/>
		<!--雇员和雇员任务一对多级联-->
		<collection property="employeeTaskList" column="id"
					fetchType="eager"
					select="com.ssmr.chapter05.dao.EmployeeTaskDao.getEmployeeTaskByEmpId"/>
		<!--鉴别器级联-->
		<discriminator javaType="long" column="sex">
			<case value="0" resultMap="maleHealthFormMapper" />
			<case value="1" resultMap="femaleHealthFormMapper"/>
		</discriminator>
	</resultMap>

	<resultMap id="femaleHealthFormMapper" type="com.ssmr.chapter05.pojo.FemaleEmployee" extends="employeeMap">
		<!--女性和女性体检表一对一级联-->
		<association property="femaleHealthForm" column="id"
					 select="com.ssmr.chapter05.dao.FemaleHealthFormDao.getFemaleHealthForm"/>
	</resultMap>
	<resultMap id="maleHealthFormMapper" type="com.ssmr.chapter05.pojo.MaleEmployee" extends="employeeMap">
		<!--男性和男性体检表一对一级联-->
		<association property="maleHealthForm" column="id"
					 select="com.ssmr.chapter05.dao.MaleHealthFormDao.getMaleHealthForm"/>
	</resultMap>

	<select id="getEmployee" parameterType="long" resultMap="employeeMap">
		select id, real_name as realName, sex, birthday, mobile, email, position,note
		from t_employee
		where id = #{id}
	</select>
</mapper>

3.N+1问题

级联完成后,只要加载主信息,一些关联信息也会同时加载,有些我们是暂时不用的,这会造成浪费,服务器压力也会增大,这就是N+1问题。解决方法就是延迟加载。mybatis提供了延迟加载的功能。

4.延迟加载

在MyBatis的配置文件mybatis-config.xml中settings配置中存在两个元素可以配置级联:

lazyLoadingEnabled 延迟加载的全局开关,默认false。

aggressiveLazyLoading  延迟加载的层级开关。版本3.4.1(包含)之前默认true,之后为false。

       这两个属性都是全局配置。如果不开启的话会把级联的所有数据加载;如果都开启,就是加载层级数据,如雇员下有雇员任务和工卡,aggressiveLazyLoading为true的话就会将这两个级联数据都加载出来,为false的话就都不加载出来。现在我们只想加载雇员任务而不加载工卡。怎么办呢?

<settings>
	<!--延迟加载的开关-->
	<setting name="lazyLoadingEnabled" value="true"/>
	<!--层级延迟加载的开关。版本3.4.1(包含)之前为true,之后为false-->
	<setting name="aggressiveLazyLoading" value="false"/>
</settings>
fetchType属性会处理全局定义无法处理的问题。fetchType出现在级联元素(association、collection。注意:discriminator没有这个属性)中,它存在两个值:

   eager:获得当前的pojo后立即加载对应的数据。

   lazy:获得当前pojo后延迟加载对应的数据。

fetch属性会忽略全局配置项lazyLoadingEnabled和aggressiveLazyLoading。


5.另一种级联

这种方式会消除N+1问题,但是会引入其他问题:SQL复杂、配置复杂、一次性取出数据会浪费内存、维护困难。

这里请读者点击链接自行查看:另一种级联DEMO

6.多对多级联

往往会拆分成两个一对多的级联。

1.POJO

public class Role{
    private Long id;
    private String roleName;
    private String note;
    //关联用户信息,一对多关联
    private List<User> userList;
    /** getter/setter **/
}  
public class User {
    private Long id;
    private String userName;
    private String realName;
    private SexEnum sex;
    private String moble;
    private String email;
    private String note;
    // 关联角色对象,一对多关联
    private List<Role> roleList;
    /** getter/setter **/
}  
2.mapper.xml  在映射器设置fetchType为lazy,这样就不会立即加载数据进来

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ssmr.chapter05.dao.RoleDao">
	<resultMap type="com.ssmr.chapter05.pojo.Role" id="roleMapper">
		<id column="id" property="id" />
		<result column="role_name" property="roleName" />
		<result column="note" property="note" />
		<collection property="userList" column="id"
					fetchType="lazy"
					select="com.ssmr.chapter05.mapper.UserMapper.findUserByRoleId" />
	</resultMap>

	<select id="getRole" parameterType="long" resultMap="roleMapper">
		select id, role_name, note
		from t_role
		where id = #{id}
	</select>

	<select id="findRoleByUserId" parameterType="long" resultMap="roleMapper">
		select r.id, r.role_name, r.note
		from t_role r, t_user_role ur
		where r.id = ur.role_id and ur.user_id = #{userId}
	</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ssmr.chapter05.mapper.UserMapper">
	<resultMap type="com.ssmr.chapter05.pojo.User" id="userMapper">
		<id column="id" property="id" />
		<result column="user_name" property="userName" />
		<result column="real_name" property="realName" />
		<result column="sex" property="sex"
			typeHandler="com.ssmr.chapter05.typehandler.SexTypeHandler" />
		<result column="mobile" property="moble" />
		<result column="email" property="email" />
		<result column="position" property="position" />
		<result column="note" property="note" />
		<collection property="roleList" column="id"
					fetchType="lazy"
			select="com.ssmr.chapter05.mapper.RoleMapper.findRoleByUserId" />
	</resultMap>
	<select id="getUser" parameterType="long" resultMap="userMapper">
		select id, user_name, real_name, sex, moble, email, note
		from t_user
		where id =#{id}
	</select>
	<select id="findUserByRoleId" parameterType="long" resultMap="userMapper">
		select u.id, u.user_name, u.real_name, u.sex, u.moble, u.email, u.note
		from t_user u , t_user_role ur
		where u.id = ur.user_id and ur.role_id =#{roleId}
	</select>
</mapper>

八、缓存

1.概述

MyBatis分为一级缓存和二级缓存。

一级缓存是在SqlSession上的缓存,二级缓存是在SqlSessionFactory上的缓存。默认情况下,也就是没有任何配置的情况下,MyBatis系统会开启以及缓存,也就是对SqlSession层面的缓存,这个缓存不需要POJO对象可序列化。

2.一级缓存

对同一对象进行两次获取,如果第二次的SQL和参数都没有变化,并且缓存没有超时或者声明需要刷新时,那么它就会从缓存中取数据。

public static void testOneLevelCache() {
    SqlSession sqlSession = null;
    Logger logger = Logger.getLogger(Test.class);
    try {
        sqlSession = SqlSessionFactoryUtil.openSqlSession();
        RoleDao2 roleDao2 = sqlSession.getMapper(RoleDao2.class);
        Role2 role = roleDao2.getRole(1L);
        logger.info("再获取一次POJO......");
        Role2 role2 = roleDao2.getRole(1L);
    } catch(Exception ex) {
        ex.printStackTrace();
    } finally {
        if (sqlSession != null) {
            sqlSession.close();
        }
    }
}

3.二级缓存

对于不同的SqlSession对象,一级缓存是不能共享的。如下代码会执行两次SQL:

public static void testTwoLevelCache() {
    SqlSession sqlSession = null;
    SqlSession sqlSession2 = null;
    Logger logger = Logger.getLogger(Test.class);
    try {
        sqlSession = SqlSessionFactoryUtil.openSqlSession();
        sqlSession2 = SqlSessionFactoryUtil.openSqlSession();
        RoleDao2 roleDao2 = sqlSession.getMapper(RoleDao2.class);
        Role2 role2 = roleDao2.getRole(1L);
        //需要提交,如果是一级缓存,MyBatis才会缓存对象到SqlSessionFactory层面
        sqlSession.commit();
        logger.info("不同sqlSession再获取一次POJO......");
        RoleDao2 roleDao22 = sqlSession2.getMapper(RoleDao2.class);
        Role2 role22 = roleDao22.getRole(1L);
        //需要提交,MyBatis才缓存对象到SQLSessionFactory
        sqlSession2.commit();
    } catch(Exception e) {
        logger.info(e.getMessage(), e);
    } finally {
        if (sqlSession != null) {
            sqlSession.close();
        }
        if (sqlSession2 != null) {
            sqlSession.close();
        }
    }
}

为了使SqlSession对象之间共享相同的缓存,有时候需要开启二级缓存。

开启二级缓存只需要在RoleMapper.xml中加入下面代码:

<cache/>
这个时候MyBatis会序列化和反序列化对应的POJO,也就要求POJO是一个可序列化的对象,那么它就必须实现java.io.Serializable接口。对角色类Role对象进行缓存,那么就需要它实现Serializable接口。

4.缓存配置项、自定义和引用

测试二级缓存,只配置cache元素,加入这个元素后,MyBatis就会将对应的命名空间内所有select元素SQL查询结果进行缓存,而其中的insert、delete、update语句在操作时会刷新缓存。

<cache 
	blocking=""
	readOnly=""	
	eviction=""
	flushInterval=""
	type=""
	size=""
/>
(1)type自定义缓存类,实现Cache接口即可。

在现实中,我们可以使用Redis、MongoDB或者其它常用的缓存,假设存在一个Redis的缓存实现类com.ssmr.chapter05.cache.RedisCache.那么可以这样配置:

<cache type="com.ssmr.chapter05.cache.RedisCache.">
	<property name="host" value="localhost"/>
</cache>
这样配置后,MyBatis会启用缓存,同时调用setHost(String host)方法,去设置配置的内容。
(2)对于一些语句也需要自定义。比如对于一些查询并不想要它进行任何缓存,这时就可以通过配置改变。
<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
以上是默认配置。flushCache代表是否刷新缓存。useCache属性是select特有的,代表是否需要使用缓存。

注意:这些缓存配置都是在一个映射器内配置的,如果其他映射器需要使用同样的配置,则可以引入缓存的配置:

<cache-ref namespace="com.ssmr.chapter05.dao.RoleDao"/>

九、存储过程

1.简述

     存储过程是数据库预先编译好,放在数据库内存中的一个程序片段,性能高,可重复使用。

     三种类型参数:输入参数IN、输出参数OUT、输入输出参数INOUT。

2.IN和OUT参数存储过程

这里使用Oracle数据库。

(1)场景:根据角色名称进行模糊查询其总数,然后把总数和查询日期返回给调用者。

(2)准备

存储过程如下:

CREATE OR REPLACE
PROCEDURE count_role(
	p_role_name IN VARCHAR,
	count_total OUT INT,
	exec_date OUT DATE)
IS
BEGIN
SELECT COUNT(*) INTO count_total FROM t_role WHERE role_name LIKE '%' || p_role_name || '%';
SELECT SYSDATE INTO exec_date FROM DUAL;
END;
创建一个POJO:
public class PdCountRoleParams {
	private String roleName;
	private int total;
	private Date execDate;
	
	/** getter/setter **/
}

(3)使用

<select id="countRole" parameterType="com.ssmr.chapter05.pojo.PdCountRoleParams" statementType="CALLABLE">
	{call count_role(
	#{roleName, mode=IN, jdbcType=VARCHAR},
	#{total, mode=OUT, jdbcType=INTEGER},
	#{execDate, mode=OUT, jdbcType=DATE}
	)}
</select>
statementType为CALLABLE,说明它是在使用存储过程,不这样声明就会抛异常。

在属性上通过model设置了其输入或者输出参数,指定对应jdbcType,这样mybatis就会使用对应的typeHandler去处理对应的类型转换。

3.游标的使用

如果把jdbcType声明为CURSOR,那么它就会使用ResultSet对象处理对应的结果。

(1)场景:同样根据角色名称模糊查询角色表的数据,但要求分页查询,于是存在start和end两个参数。为了知道是否存在下一页,还会要求查出总数。

(2)准备

存储过程:

CREATE OR REPLACE
PROCEDURE find_role(
	p_role_name IN VARCHAR,
	p_start IN INT,
	p_end IN INT,
	r_count OUT INT,
	ref_cur OUT sys_refcursor)
AS
BEGIN
SELECT COUNT(*) INTO r_count FROM t_role WHERE role_name LIKE '%' || p_role_name || '%';
OPEN ref_cur FOR
SELECT id,role_name,note FROM
	(SELECT id,role_name,note,rownum AS row1 FROM t_role a
		WHERE a.role_name LIKE '%' || p_role_name || '%' AND rownum <=p_end)
WHERE row1>p_start;
END find_role;

定义存储游标的POJO:

参数是和存储过程一一对应的,而游标是由roleList去存储的,使用时只需为其提供映射关系即可。

public class PdFindRoleParams {
	private String roleName;
	private int start;
	private int end;
	private int total;
	private List<Role> roleList;

	/** getter/setter **/
}

(3)使用

<resultMap type="com.ssmr.chapter05.pojo.Role" id="roleMapper">
	<id column="id" property="id" />
	<result column="role_name" property="roleName" />
	<result column="note" property="note" />
</resultMap>
<select id="findRole" parameterType="com.ssmr.chapter05.param.PdFindRoleParams" statementType="CALLABLE">
	{call find_role(
	#{roleName, mode=IN, jdbcType=VARCHAR},
	#{start, mode=IN,jdbcType=INTEGER},
	#{end, mode=IN, jdbcType=INTEGER},
	#{total, mode=OUT,jdbcType=INTEGER},
	#{roleList,mode=OUT,jdbcType=CURSOR,
		javaType=ResultSet,resultMap=roleMap}
	)}
</select>
为了使得ResultSet对应能够映射POJO,设置resultMap为roleMap。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值