java数据持久化介绍_Java数据持久化之mybatis(11)

Java数据持久化之mybatis

一. mybatis简介

mybatis是一个简化和实现了Java数据持久层的开源框架,它抽象了大量的JDBC冗余代码,并且提供简单易用的api和数据库交互.mybatis的前身是ibatis,ibatis由Clinton Begin 创建.mybatis3是ibatis的全新设计,支持注解和mapper.

MyBatis 流行的主要原因在于它的简单性和易使用性。在 Java 应用程序中,数据持久化层涉及到的工作有:将从数 据库查询到的数据生成所需要的 Java 对象;将 Java 对象中的数据通过 SQL 持久化到数据库中。

MyBatis 通过抽象底层的JDBC代码,自动化SQL结果集产生Java对象、Java对象的数据持久化数据库中的过程 使得对 SQL 的使用变得容易。

1.1 原始的JDBC操作: Java 通过 Java 数据库连接(Java DataBase Connectivity,JDBC)API 来操作关系型数据库,但是 JDBC 是一个

非常底层的 API,我们需要书写大量的代码来完成对数据库的操作。

让我们演示一下我们是怎样使用纯的 JDBC 来对表 STUDENTS 实现简单的 select 和 insert 操作的。

假设表 STUDENTS 有 STUD_ID,NAME,EMAIL 和 DOB 字段。对应的 Student JavaBean 定义如下:

public class Student {

private Integer studId;

private String name;

private String email;

private Date dob;

//setter and getter

}

下面的StudentService实现了通过JDBC对表students的操作

public Student findStudentById(Integer studId) {

Student student = null;

Connection connection = null;

try {

connection = getDataBaseConnection();

String sql = "select * from students where stud_id=?";

PreparedStatement stamte = connection.prepareStatement(sql);

stamte.setInt(1, studId);

ResultSet rSet = stamte.executeQuery();

if (rSet.next()) {

student = new Student();

student.setStudId(rSet.getInt("stud_id"));

student.setName(rSet.getString("name"));

student.setEmail("email");

student.setDob(rSet.getDate("dob"));

}

} catch (Exception e) {

// TODO: handle exception

throw new RuntimeException(e);

}finally {

if (connection != null) {

try {

connection.close();

} catch (Exception e2) {

// TODO: handle exception

throw new RuntimeException(e2);

}

}

}

return student;

}

public void createStudent(Student student) {

Connection connection = null;

try {

String sql = "insert into students(stud_id,name,email,dob) "

+ "values(?,?,?,?)";

connection = getDataBaseConnection();

PreparedStatement preparedStatement = connection.prepareStatement(sql);

preparedStatement.setInt(1, student.getStudId());

preparedStatement.setString(2, student.getName());

preparedStatement.setString(3, student.getEmail());

preparedStatement.setDate(4, new java.sql.Date(student.getDob().getTime()));

preparedStatement.executeUpdate();

} catch (SQLException e) {

// TODO Auto-generated catch block

e.printStackTrace();

throw new RuntimeException(e);

}finally {

if (connection!=null) {

try {

connection.close();

} catch (Exception e2) {

// TODO: handle exception

throw new RuntimeException(e2);

}

}

}

}

protected Connection getDataBaseConnection() throws SQLException {

try {

Class.forName("com.mysql.jdbc.Driver");

return DriverManager.getConnection("jdbc:mysql://localhost:3306"

+ "/mybatis", "root", "1l9o9v0e");

} catch (Exception e) {

// TODO: handle exception

throw new RuntimeException(e);

}

}

上面的每一步操作都有大量的重复代码:创建一个连接,创建一个statement对象,设置参数,关闭资源. mybatis抽象了上述的这些相同的任务:如准备需要被执行的SQL statement对象并且将Java对象作为输入数据 传递给 statement 对象的任务,进而开发人员可以专注于真正重要的方面。

另外,MyBatis 自动化了将从输入的 Java 对象中的属性设置成查询参数、从 SQL 结果集上生成 Java 对象这两个过 程。

现在让我们看看怎样通过 MyBatis 实现上述的方法:

在 SQL Mapper映射配置文件中配置SQL语句,假定文件为StudentMapper.xml

SELECT STUD_ID AS STUDID,NAME,EMAIL,DOB

FROM STUDENTS WHERE STUD_ID=#{Id}

INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL,DOB)

VALUES(#{studId},#{name},#{email},#{dob}))

创建一个StudentMapper接口

public interface StudentMapper {

Student findStudentById(Integer id);

void insertStudent(Student student);

}

在Java代码操作中,可以使用以下代码方式触发SQL语句

SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();

try {

StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

return studentMapper.findStudentById(studId);

} finally {

// TODO: handle finally clause

sqlSession.close();

}

//或者

SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();

try {

StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

studentMapper.insertStudent(student);

sqlSession.commit();

} finally {

// TODO: handle finally clause

sqlSession.close();

}

看起来和原生的JDBC操作简单了许多:不需要创建Connection和PreparedStatement,不需要自己对每一次数据库操作进行手动设 置参数和关闭连接。只需要配置数据库连接属性和 SQL 语句,

MyBatis 会处理这些底层工作。

另外,MyBatis 还提供了其他的一些特性来简化持久化逻辑的实现:

它支持复杂的SQL结果集数据映射到嵌套对象图结构

它支持一对一和一对多的结果集和Java对象的映射

它支持根据输入的数据构建动态的SQL语句

1.2 mybatis的安装和配置

1.2.1 新建一个maven项目,在pom.xml添加依赖

org.mybatis

mybatis

3.4.4

mysql

mysql-connector-java

5.1.22

runtime

org.slf4j

slf4j-api

1.7.25

org.slf4j

slf4j-log4j12

1.7.25

runtime

log4j

log4j

1.2.17

runtime

junit

junit

4.11

test

1.2.2 新建students表,插入测试数据

CREATE TABLE `students` (

`stud_id` int(11) NOT NULL AUTO_INCREMENT,

`name` varchar(50) NOT NULL,

`email` varchar(50) NOT NULL,

`dob` date DEFAULT NULL,

PRIMARY KEY (`stud_id`),

) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

insert into students(`name`,email,dob) values ('John','john@gmail.com','1990-09-09');

insert into tutors(`name`,email,dob) values ('Ying','ying@gmail.com','1990-09-09');

新建log4j.properties文件到resources文件夹

log4j.rootLogger=DEBUG, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender

log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

log4j.appender.stdout.layout.ConversionPattern=%d [%-5p] %c - %m%n

1.2.3 新建mybatis-config.xml和StudentMapper.xml配置文件

mybatis-config.xml包括数据库连接信息,类型别名等等;StudentMapper.xml包含了映射的 SQL 语句

mybatis-config.xml

StudentMapper.xml

SELECT * FROM STUDENTS

SELECT STUD_ID AS STUDID,NAME,EMAIL,DOB

FROM STUDENTS WHERE STUD_ID=#{Id}

INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL,DOB)

VALUES(#{studId},#{name},#{email},#{dob}))

StudentMapper,xml 文件包含的映射的 SQL 语句可以通过 ID 加上名空间调用。

1.2.4 新建 MyBatisSqlSessionFactory 单例类 使其持有一个 SqlSessionFactory 单例对象:

package com.mrq.util;

import java.io.IOException;

import java.io.InputStream;

import org.apache.ibatis.io.Resources;

import org.apache.ibatis.session.SqlSession;

import org.apache.ibatis.session.SqlSessionFactory;

import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class MyBatisSqlSessionFactory {

private static SqlSessionFactory sqlSessionFactory;

private static ThreadLocal threadLocal = new ThreadLocal();

static{

try {

InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");

sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

System.out.println(inputStream+"---->");

} catch (IOException e) {

// TODO: handle exception

e.getSuppressed();

throw new RuntimeException(e);

}

}

public static SqlSession openSession() {

SqlSession sqlSession = threadLocal.get();

if (sqlSession==null) {

sqlSession = sqlSessionFactory.openSession();

threadLocal.set(sqlSession);

}

return sqlSession;

}

public static void closeSqlSession() {

SqlSession sqlSession = threadLocal.get();

if (sqlSession!=null) {

sqlSession.close();

threadLocal.remove();

}

}

private MyBatisSqlSessionFactory() {

// TODO Auto-generated constructor stub

}

}

1)在静态初始化块中加载mybatis配置文件和StudentMapper.xml文件一次

2)使用ThreadLocal对象让当前线程与SqlSession对象绑定在一起

3)获取当前线程中的SqlSession对象,如果没有的话,从SqlSessionFactory对象中获取SqlSession对象

4)获取当前线程中的SqlSession对象,再将其关闭,释放其占用的资源

1.2.5 新建StudentMapper接口和StudentService类

StudentMapper.java

package com.mrq.mappers;

import java.util.List;

import com.mrq.domain.Student;

public interface StudentMapper {

List findAllStudents();

Student findStudentById(Integer id);

void insertStudent(Student student);

}

StudentService.java

package com.mrq.service;

import static org.hamcrest.CoreMatchers.nullValue;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.List;

import org.apache.ibatis.session.SqlSession;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import com.mrq.domain.Student;

import com.mrq.mappers.StudentMapper;

import com.mrq.util.MyBatisSqlSessionFactory;

public class StudentService {

private Logger logger = LoggerFactory.getLogger(getClass());

public List findAllStudents() {

SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();

try {

StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

return studentMapper.findAllStudents();

} finally {

sqlSession.close();

}

}

public Student findStudentById(Integer studId) {

logger.debug("Select Student by Id:{}",studId);

SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();

try {

StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

return studentMapper.findStudentById(studId);

} finally {

// TODO: handle finally clause

sqlSession.close();

}

}

public void createStudent(Student student) {

SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();

try {

StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

studentMapper.insertStudent(student);

sqlSession.commit();

} finally {

// TODO: handle finally clause

sqlSession.close();

}

}

}

也可以通过sqlSession的api执行SQL语句

Student student = (Student)sqlSession.

selectOne("com.mrq.mappers.StudentMapper.findStudentById",

studId);

1.2.6 创建测试Service类测试

private static StudentService studentService;

static{

studentService = new StudentService();

}

@Test

public void testFindAllStudents() {

List students = studentService.findAllStudents();

Assert.assertNotNull(students);

for (Student student : students) {

System.out.println(student);

}

}

@Test

public void testFindStudentById()

{

Student student = studentService.findStudentById(5);

Assert.assertNotNull(student);

System.out.println(student);

}

@Test

public void testCreateStudent()

{

Student student = new Student();

int id = 5;

student.setStudId(id);

student.setName("student_" + id);

student.setEmail("student_" + id + "@didihu.com");

student.setDob(new Date());

studentService.createStudent(student);

Student newStudent = studentService.findStudentById(id);

Assert.assertNotNull(newStudent);

}

1.2.7 mybatis的工作流程

首先,我们配置了 MyBatis 最主要的配置文件-mybatis-config.xml,里面包含了 JDBC 连接参数;配置了映射器Mapper XML 配置文件文件,里面包含了 SQL 语句的映射。

我们使用 mybatis-config.xml 内的信息创建了 SqlSessionFactory 对象。每个数据库环境应该就一个

SqlSessionFactory 对象实例,所以我们使用了单例模式只创建一个 SqlSessionFactory 实例。

我们创建了一个映射器 Mapper 接口-StudentMapper,其定义的方法签名和在 StudentMapper.xml 中定义的完全 一样(即映射器 Mapper 接口中的方法名跟 StudentMapper.xml 中的 id 的值相同)。注意 StudentMapper.xml 中 namespace 的值被设置成 com.mrq.mappers.StudentMapper,是 StudentMapper 接口的完全限定名。这使我们 可以使用接口来调用映射的 SQL 语句。

在 StudentService.java 中,我们在每一个方法中创建了一个新的 SqlSession,并在方法功能完成后关闭 SqlSession。每一个线程应该有它自己的 SqlSession 实例。SqlSession 对象实例不是线程安全的,并且不被共享。所 以 SqlSession 的作用域最好就是其所在方法的作用域。从 Web 应用程序角度上看,SqlSession 应该存在于 request 级 别作用域上。

二. mybatis配置主要内容

MyBatis 最关键的组成部分是 SqlSessionFactory,我们可以从中获取 SqlSession,并执行映射的 SQL 语句。

SqlSessionFactory 对象可以通过基于 XML 的配置信息或者 Java API 创建。

我们将探索各种 MaBatis 配置元素,如 dataSource,environments,全局参数设置,typeAlias,typeHandlers,

SQL 映射;接着我们将实例化 SqlSessionFactory。

2.1 使用 XML 配置 MyBatis

构建 SqlSessionFactory 最常见的方式是基于 XML 配置(的构造方式)。下面的 mybatis-config.xml 展示了一个 典型的 MyBatis 配置文件的样子:

下面让我们逐个讨论上述配置文件的组成部分,先从最重要的部分开始,即 environments:

2.1.1 environment

MyBatis 支持配置多个 dataSource 环境,可以将应用部署到不同的环境上,如 DEV(开发环境),TEST(测试换将), QA(质量评估环境),UAT(用户验收环境),PRODUCTION(生产环境),可以通过将默认 environment 值设置成想要的 environment id 值。

在上述的配置中,默认的环境 environment 被设置成 development。当需要将程序部署到生产服务器上时,你不需 要修改什么配置,只需要将默认环境environment值设置成生产环境的 environmentid属性即可。

有时候,我们可能需要在相同的应用下使用多个数据库。比如我们可能有 SHOPPING-CART 数据库来存储所有的订单 明细;使用 REPORTS 数据库存储订单明细的合计,用作报告。

如果你的应用需要连接多个数据库,你需要将每个数据库配置成独立的环境,并且为每一个数据库创建一个 SqlSessionFactory。

我们可以如下为每个环境创建一个 SqlSessionFactory:

inputStream = Resources.getResourceAsStream("mybatis-config.xml");

defaultSqlSessionFactory = new SqlSessionFactoryBuilder().

build(inputStream);

cartSqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, "shoppingcart");

reportSqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, "reports");

创建 SqlSessionFactory 时,如果没有明确指定环境 environment id,则会使用默认的环境 environment 来创 建。在上述的源码中,默认的 SqlSessionFactory 便是使用 shoppingcart 环境设置创建的。

对于每个环境 environment,我们需要配置 dataSource 和 transactionManager 元素。

2.1.2 数据源DataSource

dataSource 元素被用来配置数据库连接属性。

使用连接池

class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">

dataSource 的类型可以配置成其内置类型之一,如 UNPOOLED,POOLED,JNDI。

如果将类型设置成UNPOOLED,MyBatis会为每一个数据库操作创建一个新的连接,并关闭它。该方式适用于只有小规模数量并发用户的简单应用程序上。

如果将属性设置成POOLED,MyBatis会创建一个数据库连接池,连接池中的一个连接将会被用作数据库操作。一旦数据库操作完成,MyBatis 会将此连接返回给连接池。在开发或测试环境中,经常使用此

种方式。

如果将类型设置成JNDI,MyBatis从在应用服务器向配置好的JNDI数据源dataSource获取数据库连接。在生产环境中,优先考虑这种方式。

2.1.3 事务管理器 TransactionManager

MyBatis 支持两种类型的事务管理器: JDBC and MANAGED.

JDBC事务管理器被用作当应用程序负责管理数据库连接的生命周期(提交、回退等等)的时候。当你将TransactionManager 属性设置成 JDBC,MyBatis 内部将使用 JdbcTransactionFactory 类创建TransactionManager。例如,部署到 Apache Tomcat 的应用程序,需要应用程序自己管理事务。

MANAGED 事务管理器是当由应用服务器负责管理数据库连接生命周期的时候使用。当你将 TransactionManager 属性设置成 MANAGED 时,MyBatis 内部使用 ManagedTransactionFactory 类

创建事务管理器TransactionManager。例如,当一个JavaEE的应用程序部署在类似 JBoss,WebLogic, GlassFish 应用服务器上时,它们会使用 EJB 进行应用服务器的事务管理能力。在这些管理环境中,你 可以使用 MANAGED 事务管理器。

(译者注:Managed 是托管的意思,即是应用本身不去管理事务,而是把事务管理交给应用所在的服务 器进行管理。)

2.1.4 属性properties

属性配置元素可以将配置值具体化到一个属性文件中,并且使用属性文件的 key 名作为占位符。在上述的配置中,我 们将数据库连接属性具体化到了 application.properties 文件中,并且为 driver,URL 等属性使用了占位符。

在 applications.properties 文件中配置数据库连接参数,如下所示:

jdbc.driverClassName=com.mysql.jdbc.Driver

jdbc.url=jdbc:mysql://localhost:3306/mybatis02

jdbc.username=root

jdbc.password=1l9o9v0e

在 mybatis-config.xml 文件中,为属性使用 application.properties 文件中定义的占位符:

并且,你可以在元素中配置默认参数的值。如果中定义的元素和属性文件定义元素的 key 值相同,它们会被属性文件中定义的值覆盖。

这里,如果 application.properties 文件包含值 jdbc.username 和 jdbc.password,则上述定义的 username 和 password 的值 db_user 和 verysecurepwd 将会被 application.properties 中定义的对应的 jdbc.username 和 jdbc.password 值覆盖。

2.1.5 类型别名 typeAliases:

在 SQLMapper 配置文件中,对于 resultType 和 parameterType 属性值,我们需要使用 JavaBean 的完全限定名。

这里我们为 resultType 和 parameterType 属性值设置为 Student 类型的完全限定名: com.mrq.domain.Student

我们可以为完全限定名取一个别名(alias),然后其需要使用完全限定名的地方使用别名,而不是到处使用完全限定 名。如下例子所示,为完全限定名起一个别名.然后在 SQL Mapper 映射文件中, 如下使用 Student 的别名:

SELECT STUD_ID AS STUDID,NAME,EMAIL,DOB

FROM STUDENTS WHERE STUD_ID=#{Id}

update students set name=#{name},email=#{email},phone=#{phone}

where stud_id=#{studId}

我们可以不用为每一个JavaBean单独定义别名, 你可以为提供需要取别名的JavaBean所在的包(package),MyBatis 会自动扫描包内定义的 JavaBeans,然后分别为 JavaBean 注册一个小写字母开头的非完全限定的类名形式的别名。如下所 示,提供一个需要为 JavaBeans 起别名的包名:

如果 Student.java 和 Tutor.java Bean 定义在 com.mrq.domain 包中,则 com.mrq.domain.Student 的别名会被注册为 student。而 com.mrq.domain.Tutor 别名将会被注册为 tutor.

还有另外一种方式为 JavaBeans 起别名,使用注解@Alias:

@Alias("StudentAlias")

public class Student

{

}

@Alias 注解将会覆盖配置文件中的定义。

2.1.6 类型处理器 typeHandlers

如上一章已经讨论过,MyBatis 通过抽象 JDBC 来简化了数据持久化逻辑的实现。MyBatis 在其内部使用 JDBC,提供了更简洁的方式实现了数据库操作。

当 MyBatis 将一个 Java 对象作为输入参数执行 INSERT 语句操作时,它会创建一个 PreparedStatement 对象,并且 使用 setXXX()方式对占位符设置相应的参数值。

这里,XXX 可以是 Int,String,Date 等 Java 对象属性类型的任意一个。示例如下:

INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL,DOB)

VALUES(#{studId},#{name},#{email},#{dob})

为执行这个SQL语句,mybatis将会执行下面的步骤

创建一个有占位符的PreparedStatement 接口,如下:

PreparedStatement pstmt = connection.prepareStatement("INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL,DOB) VALUES(?,?,?,?)");

检查Student对象的属性studId的类型,然后使用合适setXXX方法去设置参数值。这里studId是integer 类型,所以会使用 setInt()方法:

pstmt.setInt(1,student.getStudId());

类似地,对于name和email属性都是String类型,MyBatis使用setString()方法设置参数。

pstmt.setString(2, student.getName());

pstmt.setString(3, student.getEmail());

4.对于dob属性,MyBatis会使用setDate()方法设置dob处占位符位置的值。

5.MyBaits 会将 java.util.Date 类型转换为 java.sql.Timestamp 并设值:

但 MyBatis 是怎么知道对于 Integer 类型属性使用 setInt() 和 String 类型属性使用 setString()方法呢? 其实 MyBatis 是通过使用类型处理器(type handlers)来决定这么做的。

MyBatis 对于以下的类型使用内建的类型处理器:所有的基本数据类型、基本类型的包裹类型、byte[]、 java.util.Date、java.sql.Date、java,sql.Time、java.sql.Timestamp、java 枚举类型等。所以当 MyBatis 发现 属性的类型属于上述类型,他会使用对应的类型处理器将值设置到 PreparedStatement 中,同样地,当从 SQL 结果集构 建 JavaBean 时,也有类似的过程.

那如果我们给了一个自定义的对象类型,来存储存储到数据库呢?示例如下:

假设表STUDENTS有一个PHONE字段,类型为VARCHAR(15),而JavaBean Student有一个PhoneNumber类定义类 型的 phoneNumber 属性

package com.mrq.domain;

public class PhoneNumber {

private String countryCode;

private String stateCode;

private String number;

public PhoneNumber() {

// TODO Auto-generated constructor stub

}

public PhoneNumber(String countryCode, String stateCode, String number) {

super();

this.countryCode = countryCode;

this.stateCode = stateCode;

this.number = number;

}

public PhoneNumber(String string){

if (string!=null) {

String[] parts = string.split("-");

if (parts.length>0) {

this.countryCode = parts[0];

}

if (parts.length>1) {

this.stateCode = parts[1];

}

if (parts.length>2) {

this.number = parts[2];

}

}

}

public String getAsString() {

return countryCode+"-"+stateCode+"-"+number;

}

public String getCountryCode() {

return countryCode;

}

public void setCountryCode(String countryCode) {

this.countryCode = countryCode;

}

public String getStateCode() {

return stateCode;

}

public void setStateCode(String stateCode) {

this.stateCode = stateCode;

}

public String getNumber() {

return number;

}

public void setNumber(String number) {

this.number = number;

}

}

public class Student

{

private Integer id;

private String name;

private String email;

private PhoneNumber phone;

// Setters and getters

}

mappeing

insert into students(name,email,phone)

values(#{name},#{email},#{phone})

这里,phone 参数需要传递给#{phone};而 phone 对象是 PhoneNumber 类型。然而,MyBatis 并不知道该怎样来处 理这个类型的对象。

为了让 MyBatis 明白怎样处理这个自定义的 Java 对象类型,如 PhoneNumber,我们可以创建一个自定义的类型处理 器,如下所示:

MyBatis 提供了抽象类 BaseTypeHandler ,我们可以继承此类创建自定义类型处理器。

package com.mrq.handlers;

import java.sql.CallableStatement;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import org.apache.ibatis.type.BaseTypeHandler;

import org.apache.ibatis.type.JdbcType;

import com.mrq.domain.PhoneNumber;

public class PhoneTypeHandler extends BaseTypeHandler{

@Override

public PhoneNumber getNullableResult(ResultSet rs, String columnName) throws SQLException {

// TODO Auto-generated method stub

return new PhoneNumber(rs.getString(columnName));

}

@Override

public PhoneNumber getNullableResult(ResultSet rs, int columnIndex) throws SQLException {

// TODO Auto-generated method stub

return new PhoneNumber(rs.getString(columnIndex));

}

@Override

public PhoneNumber getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {

// TODO Auto-generated method stub

return new PhoneNumber(cs.getString(columnIndex));

}

@Override

public void setNonNullParameter(PreparedStatement ps, int i, PhoneNumber parameter, JdbcType jdbcType)

throws SQLException {

// TODO Auto-generated method stub

ps.setString(i, parameter.getAsString());

}

}

2.我们使用ps.setString()和rs.getString()方法是因为phone列是VARCHAR类型。

3.一旦我们实现了自定义的类型处理器,我们需要在mybatis-config.xml中注册它:

注册 PhoneTypeHandler 后, MyBatis 就能够将 Phone 类型的对象值存储到 VARCHAR 类型的列上。

2.1.7 全局参数设置 Settings

为满足应用特定的需求,MyBatis 默认的全局参数设置可以被覆盖(overridden)掉,如下所示:

2.1.8 SQL映射定义Mappers

Mapper XML 文件中包含的 SQL 映射语句将会被应用通过使用其 statementid 来执行。我们需要在 mybatis- config.xml 文件中配置 SQL Mapper 文件的位置。

以上每一个 标签的属性有助于从不同类型的资源中加载映射 mapper:

resource属性用来指定在classpath中的mapper文件。

url属性用来通过完全文件系统路径或者webURL地址来指向mapper文件

class属性用来指向一个mapper接口

package属性用来指向可以找到Mapper接口的包名

三. 使用 XML 配置 SQL 映射器

关系型数据库和SQL是经受时间考验和验证的数据存储机制。和其他的ORM 框架如Hibernate不同,MyBatis鼓励 开发者可以直接使用数据库,而不是将其对开发者隐藏,因为这样可以充分发挥数据库服务器所提供的 SQL 语句的巨大威 力。与此同时,MyBaits 消除了书写大量冗余代码的痛苦,它使使用 SQL 更容易。

在代码里直接嵌套 SQL 语句是很差的编码实践,并且维护起来困难。MyBaits 使用了映射器配置文件或注解来配置 SQL 语句。下面,我们会看到具体怎样使用映射器配置文件来配置映射 SQL 语句。

3.1 映射配置文件和映射接口

在前几章中,我们已经看见了一些在映射器配置文件中配置基本的映射语句,以及怎样使用 SqlSession 对象调用它们 的例子。

现在让我们看一下在 com.mrq.mappers 包中的 StudentMapper.xml 配置文件内,是如何配置 id 为” findStudentById”的 SQL 语句的,代码如下:

/p>

"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

select stud_id as studId, name, email, dob

from Students where stud_id=#{studId}

我们可以通过下列代码调用 findStudentById 映射的 SQL 语句:

public Student findStudentById(Integer studId)

{

SqlSession sqlSession = MyBatisUtil.getSqlSession();

try{

Student student =

sqlSession.selectOne("com.mrq.mappers.StudentMapper.

findStudentById", studId);

return student;

}

finally{

sqlSession.close();

}

}

MyBatis 通过使用映射器 Mapper 接口提供了更好的调用映射语句的方法。一旦我们通过映射器配置文件配置了映射语 句,我们可以创建一个完全对应的一个映射器接口,接口名跟配置文件名相同,接口所在包名也跟配置文件所在包名完全 一 样(如 StudentMapper.xml 所 在 的 包 名 是 com.mrq.mappers, 对 应 的 接 口 名 就 是com.mrq.mappers.StudentMapper.java )。映射器接口中的方法签名也跟映射器配置文件中完全对应:方法名为 配置文件中 id 值;方法参数类型为 parameterType 对应值;方法返回值类型为 returnType 对应值。

对于上述的 StudentMapper.xml 文件,我们可以创建一个映射器接口 StudentMapper.java 如下:

package com.mrq.mappers;

public interface StudentMapper

{

Student findStudentById(Integer id);

}

在StudentMapper.xml 映射器配置文件中,其名空间namespace 应该跟StudentMapper接口的完全限定名保持一 致。另外,StudentMapper.xml 中语句 id,parameterType,returnType 应该分别和 StudentMapper 接口中的方法名, 参数类型,返回值相对应.

使用映射器接口我们可以以类型安全的形式调用调用映射语句。如下所示:

public Student findStudentById(Integer studId) {

logger.debug("Select Student by Id:{}",studId);

SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();

try {

StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

return studentMapper.findStudentById(studId);

} finally {

// TODO: handle finally clause

sqlSession.close();

}

}

tip:

即使映射器 Mapper 接口可以以类型安全的方式调用映射语句,但是我们我 负责书写正确的,匹配方法名、参数类型、和返回值的映射器 Mapper 接口。 如果映射器 Mapper 接口中的方法和 XML 中的映射语句不能匹配,会在运行期 抛出一个异常。实际上,指定 parameterType 是可选的;MyBatis 可以使用反 射机制来决定 parameterType。但是,从配置可读性的角度来看,最好指定 parameterType 属性。如果 parameterType 没有被提及,开发者必须查看 Mapper XML 配置和 Java 代码了解传递给语句的输入参数的数据类

3.2 映射语句

MyBatis 提供了多种元素来配置不同类型的语句,如 SELECT,INSERT,UPDATE,DELETE。接下来让我们看看如何具体配置映射语句.

3.2.1 insert 语句:一个 INSERT SQL 语句可以在元素在映射器 XML 配置文件中配置,如下所示:

INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL, PHONE)

VALUES(#{studId},#{name},#{email},#{phone})

这里我们使用一个ID insertStudent,可以在名空间com.mrq.mappers.StudentMapper.insertStudent中 唯一标识。parameterType 属性应该是一个完全限定类名或者是一个类型别名(alias)。

我们可以如下调用这个语句:

int count =

sqlSession.insert("com.mrq.mappers.StudentMapper.insertStudent", student);

sqlSession.insert() 方法返回执行 INSERT 语句后所影响的行数。

如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型安 全的方式调用方法,如下所示:

package com.mrq.mappers;

public interface StudentMapper

{

int insertStudent(Student student);

}

可以如下调用 insertStudent 映射语句:

StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

int count = mapper.insertStudent(student);

自动生成主键

在上述的INSERT语句中,我们为可以自动生成(auto-generated)主键的列 STUD_ID插入值。我们可以使用 useGeneratedKeys 和 keyProperty属性让数据库生成 auto_increment列的值,并将生成的值设置到其中一个 输入对象属性内,如下所示:

keyProperty="studId">

INSERT INTO STUDENTS(NAME, EMAIL, PHONE) VALUES(#{name},#{email},#{phone})

这里 STUD_ID 列值将会被 MySQL 数据库自动生成,并且生成的值会被设置到 student 对象的 studId 属性上。

StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

mapper.insertStudent(student);

现在可以如下获取插入的 STUDENT 记录的 STUD_ID 的值:

int studentId = student.getStudId();

有些数据库如 Oracle 并不支持 AUTO_INCREMENT 列,其使用序列(SEQUENCE)来生成主键值。 假设我们有一个名为 STUD_ID_SEQ 的序列来生成 SUTD_ID 主键值。使用如下代码来生成主键:

SELECT ELEARNING.STUD_ID_SEQ.NEXTVAL FROM DUAL

INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL, PHONE)

VALUES(#{studId},#{name},#{email},#{phone})

这里我们使用了子元素来生成主键值,并将值保存到 Student 对象的 studId 属性上。 属性 order=“before”表示 MyBatis 将取得序列的下一个值作为主键值,并且在执行 INSERT SQL 语句之前将值设置到 studId 属性上。

我们也可以在获取序列的下一个值时,使用触发器(trigger)来设置主键值,并且在执行INSERT SQL语句之 前将值设置到主键列上。如果你采取这样的方式,则对应的 INSERT 映射语句如下所示:

INSERT INTO STUDENTS(NAME,EMAIL, PHONE)

VALUES(#{name},#{email},#{phone})

SELECT ELEARNING.STUD_ID_SEQ.CURRVAL FROM DUAL

3.2.2 UPDATE 语句

一个 UPDATE SQL 语句可以在元素在映射器 XML 配置文件中配置,如下所示:

UPDATE STUDENTS SET NAME=#{name}, EMAIL=#{email}, PHONE=#{phone}

WHERE STUD_ID=#{studId}

可以如下调用此语句:

int noOfRowsUpdated =

sqlSession.update("com.mrq.mappers.StudentMapper.updateStudent", student);

sqlSession.update() 方法返回执行 UPDATE 语句之后影响的行数。

如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型 安全的方式调用方法,如下所示:

public interface StudentMapper

{

int updateStudent(Student student);

}

可以使用映射器 Mapper 接口来调用 updateStudent 语句,如下所示:

StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

int noOfRowsUpdated = mapper.updateStudent(student);

3.2.3 删除语句

一个 DELETE SQL 语句可以在元素在映射器 XML 配置文件中配置,如下所示:

DELETE FROM STUDENTS WHERE STUD_ID=#{studId}

可以如下调用此语句:

int studId = 1;

int noOfRowsDeleted =

sqlSession.delete("com.mrq.mappers.StudentMapper.deleteStudent", studId);

sqlSession.delete() 方法返回 delete 语句执行后影响的行数。

如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型安 全的方式调用方法,如下所示:

public interface StudentMapper

{

int deleteStudent(int studId);

}

可以使用映射器 Mapper 接口来调用 deleteStudent 语句,如下所示:

StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

int noOfRowsDeleted = mapper.deleteStudent(studId);

3.2.4 select 语句

MyBatis 真正强大的功能,在于映射 SELECT 查询结果到 JavaBeans 方面的极大灵活性。 让我们看看一个简单的 select 查询是如何(在 MyBatis 中)配置的,如下所示:

resultType="Student">

SELECT STUD_ID, NAME, EMAIL, PHONE

FROM STUDENTS

WHERE STUD_ID=#{studId}

可以如下调用此语句:

int studId =1;

Student student = sqlSession.selectOne("com.mrq.mappers.

StudentMapper.findStudentById", studId);

不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型安 全的方式调用方法,如下所示:

public interface StudentMapper

{

Student findStudentById(Integer studId);

}

StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

Student student = mapper.findStudentById(studId);

如果你检查 Student 对象的属性值,你会发现 studId 属性值并没有被 stud_id 列值填充。这是因为 MyBatis 自动对 JavaBean 中和列名匹配的属性进行填充。这就是为什么 name ,email,和 phone 属性被填充,而 studId 属性没有被填 充。

为了解决这一问题,我们可以为列名起一个可以与 JavaBean 中属性名匹配的别名,如下所示:

resultType="Student">

SELECT STUD_ID AS studId, NAME,EMAIL, PHONE

FROM STUDENTS

WHERE STUD_ID=#{studId}

现在,Student 这个 Bean 对象中的值将会恰当地被 stud_id,name,email,phone 列填充了。 现在,让我们看一下如何执行返回多条结果的 SELECT 语句查询,如下所示:

SELECT STUD_ID AS studId, NAME,EMAIL, PHONE

FROM STUDENTS

List students =

sqlSession.selectList("com.mrq.mappers.StudentMapper.findAllStudents");

映射器 Mapper 接口 StudentMapper 可以如下定义:

public interface StudentMapper

{

List findAllStudents();

}

可以如下调用 findAllStudents 语句:

StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

List students = mapper.findAllStudents();

如果你注意到上述的 SELECT 映射定义,你可以看到,我们为所有的映射语句中的 stud_id 起了别名。我们可以使用 ResultMaps,来避免上述的到处重复别名。我们稍后会继续讨论。

除了 java.util.List,你也可以是由其他类型的集合类,如 Set,Map,以及(SortedSet)。MyBatis 根据集合的类型,会采用适当的集合实现,如下所示:

对于 List,Collection,Iterable 类型,MyBatis 将返回 java.util.ArrayList

对于 Map 类型,MyBatis 将返回 java.util.HashMap

对于 Set 类型,MyBatis 将返回 java.util.HashSet

对于 SortedSet 类型,MyBatis 将返回 java.util.TreeSet

3.3 结果集映射 ResultMaps

ResultMaps被用来 将SQLSELECT语句的结果集映射到 JavaBeans的属性中。我们可以定义结果集映射ResultMaps 并且在一些 SELECT 语句上引用 resultMap。MyBatis 的结果集映射 ResultMaps 特性非常强大,你可以使用它将简单的 SELECT 语句映射到复杂的一对一和一对多关系的 SELECT 语句上。

3.3.1 简单的resultMap

映射了查询结果和 Student JavaBean 的简单的 resultMap 定义如下:

SELECT * FROM STUDENTS

SELECT STUD_ID AS STUDID,NAME,EMAIL,DOB

FROM STUDENTS WHERE STUD_ID=#{Id}

表示resultMap的StudentResult id值应该在此名空间内是唯一的。并且type属性应该是完全限定类名或者是返 回类型的别名。

子元素被用来将一个 resultset 列映射到 JavaBean 的一个属性中。

元素和元素功能相同,不过它被用来映射到唯一标识属性,用来区分和比较对象(一般和主键列相对应)。

在语句中,我们使用了 resultMap 属性,而不是 resultType 来引用 StudentResult 映射。当语 句中配置了 resutlMap 属性,MyBatis 会使用此数据库列名与对象属性映射关系来填充 JavaBean 中的属性。

* resultType 和 resultMap 二者只能用其一,不能同时使用。

看另外一个select映射语句定义的例子,怎样将查询结果填充到 HashMap 中。如下所示:

SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}

在上述的select语句中,我们将 resultType 配置成 map,即 java.util.HashMap 的别名。在这种情况下,结果集 的列名将会作为 Map 中的 key 值,而列值将作为 Map 的 value 值。

HashMap studentMap = sqlSession.selectOne("com.

mrq.mappers.StudentMapper.findStudentById", studId);

System.out.println("stud_id :"+studentMap.get("stud_id"));

System.out.println("name :"+studentMap.get("name"));

System.out.println("email :"+studentMap.get("email"));

System.out.println("phone :"+studentMap.get("phone"));

再看一个 使用resultType=”map”,返回多行结果的例子:

SELECT STUD_ID, NAME, EMAIL, PHONE FROM STUDENTS

由于 resultType=”map”和语句返回多行,则最终返回的数据类型应该是 "List>",如下所 示:

List> studentMapList =

sqlSession.selectList("com.mrq.mappers.StudentMapper.findAllS

tudents");

for(HashMap studentMap : studentMapList)

{

System.out.println("studId :" + studentMap.get("stud_id"));

System.out.println("name :" + studentMap.get("name"));

System.out.println("email :" + studentMap.get("email"));

System.out.println("phone :" + studentMap.get("phone"));

}

3.3.2 拓展 ResultMap

我们可以从从另外一个,拓展出一个新的,这样,原先的属性映射可以继承过来,以实现。

id 为 StudentWithAddressResult 的 resultMap 拓展了 id 为 StudentResult 的 resultMap。 如果你只想映射 Student 数据,你可以使用 id 为 StudentResult 的 resultMap,如下所示:

resultMap="StudentResult">

SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}

如果你想将映射 Student 数据和 Address 数据,你可以使用 id 为 StudentWithAddressResult 的 resultMap:

resultMap="StudentWithAddressResult">

SELECT STUD_ID, NAME, EMAIL, PHONE, A.ADDR_ID, STREET, CITY,

STATE, ZIP, COUNTRY

FROM STUDENTS S LEFT OUTER JOIN ADDRESSES A ON

S.ADDR_ID=A.ADDR_ID

WHERE STUD_ID=#{studId}

3.4 一对一映射

在我们的域模型样例中,每一个学生都有一个与之关联的地址信息。表 STUDENTS 有一个 ADDR_ID 列,是 ADDRESSES 表的外键。

看一下怎样取 Student 明细和其 Address 明细。

Student 和 Address 的 JavaBean 以及映射器 Mapper XML 文件定义如下所示:

public class Address

{

private Integer addrId;

private String street;

private String city;

private String state;

private String zip;

private String country;

// setters & getters

}

public class Student

{

private Integer studId;

private String name;

private String email;

private PhoneNumber phone;

private Address address;

//setters & getters

}

resultMap="StudentWithAddressResult">

SELECT STUD_ID, NAME, EMAIL, A.ADDR_ID, STREET, CITY, STATE,

ZIP, COUNTRY

FROM STUDENTS S LEFT OUTER JOIN ADDRESSES A ON S.ADDR_ID=A.ADDR_ID

WHERE STUD_ID=#{studId}

我们可以使用圆点记法为内嵌的对象的属性赋值。在上述的 resultMap 中,Student 的 address 属性使用了圆点记法 被赋上了 address 对应列的值。同样地,我们可以访问任意深度的内嵌对象的属性。我们可以如下访问内嵌对象属性:

/接口定义

public interface StudentMapper

{

Student selectStudentWithAddress(int studId);

}

//使用

int studId = 1;

StudentMapper studentMapper =

sqlSession.getMapper(StudentMapper.class);

Student student = studentMapper.selectStudentWithAddress(studId);

System.out.println("Student :" + student);

System.out.println("Address :" + student.getAddress());

上述样例展示了一对一关联映射的一种方法。然而,使用这种方式映射,如果 address 结果需要在其他的 SELECT 映射 语句中映射成 Address 对象,我们需要为每一个语句重复这种映射关系。MyBatis 提供了更好地实现一对一关联映射的方 法:嵌套结果 ResultMap 和嵌套 select 查询语句。接下来,我们将讨论这两种方式。

3.4.1 使用嵌套结果ResultMap实现一对一关系映射

可以使用一个嵌套结果 ResultMap 方式来获取 Student 及其 Address 信息,代码如下:

resultMap="StudentWithAddressResult">

SELECT STUD_ID, NAME, EMAIL, A.ADDR_ID, STREET, CITY, STATE, ZIP, COUNTRY

FROM STUDENTS S LEFT OUTER JOIN ADDRESSES A ON S.ADDR_ID=A.ADDR_ID

WHERE STUD_ID=#{studId}

元素association被用来导入“有一个”(has-one)类型的关联。在上述的例子中,我们使用了association元素 引用了另外的在同一个 XML 文件中定义的resultMap。

也可以使用association 定义内联的 resultMap,代码如下所示:

使用嵌套结果 ResultMap 方式,关联的数据可以通过简单的查询语句(如果需要的话,需要与 joins 连接操作配合) 进行加载。

3.4.2 使用嵌套查询实现一对一关系映射

resultMap="AddressResult">

SELECT * FROM ADDRESSES WHERE ADDR_ID=#{id}

resultMap="StudentWithAddressResult">

SELECT * FROM STUDENTS WHERE STUD_ID=#{Id}

在此方式中,元素的 select属性被设置成了id为 findAddressById的语句。这里,两个分开的 SQL 语句将会在数据库中执行,第一个调用 findStudentById 加载 student 信息,而第二个调用 findAddressById 来 加载 address 信息。

Addr_id 列的值将会被作为输入参数传递给 selectAddressById 语句。 我们可以如下调用 findStudentWithAddress 映射语句:

StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

Student student = mapper.selectStudentWithAddress(studId);

System.out.println(student);

System.out.println(student.getAddress());

3.5 一对多映射

在我们的域模型样例中,一个讲师可以教授一个或者多个课程。这意味着讲师和课程之间存在一对多的映射关系。

我们可以使用元素将 一对多类型的结果 映射到 一个对象集合上。

数据库测试数据

insert into address(`street`,`city`,`state`,`zip`,`country`) values ('8th Street','New York','Wastern','+322','America');

insert into tutors(`name`,email,phone,`addr_id`) values ('John','john@gmail.com','123-456-7890',1);

insert into tutors(`name`,email,phone,`addr_id`) values ('Ying','ying@gmail.com','122-346-7240',2);

insert into courses(`name`,description,start_date,`end_date`,tutor_id) values ('javaSE','Java SE','2014-09-08','2015-09-07',1);

insert into courses(`name`,description,start_date,`end_date`,tutor_id) values ('javaEE','Java EE 8','2014-09-08','2015-09-07',2);

insert into courses(`name`,description,start_date,`end_date`,tutor_id) values ('Mybatis','MyBatis','2014-09-08','2015-09-07',2);

Course 和 Tutor 的 JavaBean 定义如下:

public class Course

{

private Integer courseId;

private String name;

private String description;

private Date startDate;

private Date endDate;

private Integer tutorId;

//setters & getters

}

public class Tutor

{

private Integer tutorId;

private String name;

private String email;

private Address address;

private List courses;

/ setters & getters

}

现在让我们看看如何获取讲师信息以及其所教授的课程列表信息。

collection元素被用来将多行课程结果映射成一个课程 Course 对象的一个集合。和一对一映射一样,我们可以使 用嵌套结果 ResultMap 和嵌套 Select 语句两种方式映射实现一对多映射。

3.5.1 使用内嵌结果ResultMap实现一对多映射

resultMap="TutorResult">

SELECT T.TUTOR_ID, T.NAME AS TUTOR_NAME, EMAIL, C.COURSE_ID,

C.NAME, DESCRIPTION, START_DATE, END_DATE

FROM TUTORS T LEFT OUTER JOIN ADDRESSES A ON T.ADDR_ID=A.ADDR_ID

LEFT OUTER JOIN COURSES C ON T.TUTOR_ID=C.TUTOR_ID

WHERE T.TUTOR_ID=#{tutorId}

这里我们使用了一个简单的使用了 JOINS 连接的 Select 语句获取讲师及其所教课程信息。collection元素的 resultMap 属性设置成了 CourseResult,CourseResult 包含了 Course 对象属性与表列名之间的映射。

3.5.2 使用嵌套 Select 语句实现一对多映射

可以使用嵌套 Select 语句方式获得讲师及其课程信息,代码如下:

SELECT T.TUTOR_ID, T.NAME AS TUTOR_NAME, EMAIL

FROM TUTORS T WHERE T.TUTOR_ID=#{tutorId}

SELECT * FROM COURSES WHERE TUTOR_ID=#{tutorId}

在这种方式中,元素的 select 属性被设置为 id 为 findCourseByTutor 的语句,用来触发单独 的 SQL 查询加载课程信息。tutor_id 这一列值将会作为输入参数传递给 findCouresByTutor 语句。

public interface TutorMapper

{

Tutor findTutorById(int tutorId);

}

TutorMapper mapper = sqlSession.getMapper(TutorMapper.class);

Tutor tutor = mapper.findTutorById(tutorId);

System.out.println(tutor);

List courses = tutor.getCourses();

for (Course course : courses)

{

System.out.println(course);

}

嵌套 Select 语句查询会导致 N+1 选择问题。首先,主查询将会执行(1 次),对于主 查询返回的每一行,另外一个查询将会将会被执行(主查询 N 行,则此查询 N 次)。对于 大型数据库而言,这会导致很差的性能问题。

来了就领一个红包再走吧.

55b72907345a

支付宝2.png

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值