JAVA编程讲义技术教程之 JDBC数据库技术

举报
tea_year 发表于 2022/02/06 10:31:34 2022/02/06
【摘要】 一般应用程序都会选择用数据库来存储数据,这是因为数据库能够随数据进行物理存储。应用程序要想使用数据库,就必须编写相应代码连接到数据库并进行相关操作。Java语言提供了规范客户端程序如何来访问数据库的应用程序接口,简称JDBC API,是Java数据应用开发的一项核心技术。开发人员可以使用JDBC提供的通信协议来连接和访问数据。本章首先介绍JDBC的相关概念,讲解如何利用JDBC进行数据库的添...


一般应用程序都会选择用数据库来存储数据,这是因为数据库能够随数据进行物理存储。应用程序要想使用数据库,就必须编写相应代码连接到数据库并进行相关操作。Java语言提供了规范客户端程序如何来访问数据库的应用程序接口,简称JDBC API,是Java数据应用开发的一项核心技术。开发人员可以使用JDBC提供的通信协议来连接和访问数据。本章首先介绍JDBC的相关概念,讲解如何利用JDBC进行数据库的添加、修改、删除和查询的操作,并在此基础上进一步讲解JDBC事务、DAO模式、连接池技术等Java数据库操作相关的核心技术。



14.1 JDBC概述

一般企业级的应用都需要将数据进行持久化操作。所谓持久化操作就是将内存中的数据保存到硬盘中。而持久化的实现过程大多都是通过各种关系型数据库来完成的。在Java中,操作数据库的技术就是JDBC。

14.1.1 什么是JDBC

JDBC的基础是数据库和SQL语句,没有数据库的支撑JDBC就无从谈起。

什么是数据库呢?数据库是“按照数据结构来组织、存储和管理数据的仓库”,是一个长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合。目前,市面上比较流行的关系型数据库包括MySQL、Oracle和SQLServer。

有了数据库之后,需要对数据库进行操作,在操作的时候离不开SQL语句。什么是SQL语句呢?SQL语句是对关系型数据库进行操作的一种语言,主要是用于存取数据以及查询、更新和管理数据库。

Java应用程序如果想要操作数据库就需要借助于JDBC(Java Database Connectivity) API,即Java数据库编程接口,它是一组标准的接口和类。使用这些接口和类,Java程序可以访问各种不同类型的数据库,并可以使用SQL语句来完成对数据库的添加、查询、更新和删除等操作。

对于开发者而言,JDBC提供了API,在开发的时候不需要关心实现接口的细节,直接调用即可。

14.1.2 怎样连接数据库

目前,市场上数据库产品的种类众多,最热门的是Oracle、MySQL 和 Microsoft SQL Server,占据绝大部分的市场份额,大多数开发人员都会选择这三种数据库。对于不同的数据库产品,其内部处理数据的方式不同,访问方式也不一样。那么,使用Java代码该如何连接数据库呢?针对这个问题,数据库厂商给出了解决方案:对于不同的数据库,提供了相应的数据库驱动,如图14.1所示。从图14.1中可以看出,对于MySQL数据库,只要安装MySQL驱动,JDBC就可以不用关心具体的连接过程,从而实现对MySQL的操作。Oracle数据库、SQL Server数据库,同样如此。

图14.1 厂商驱动连接数据库 图14.2 JDBC连接ODBC桥驱动

一般情况下,连接数据库的常用方式有以下两种:

• 安装相应厂商的数据库驱动。这需要去各个数据库厂商提供的官网连接下载驱动包,显得比较麻烦。

• 使用JDBC-ODBC桥驱动器。在微软公司的Windows系统中预先设定一个ODBC(Open Database Connectivity,开放数据库互联)功能,由于ODBC是微软公司的产品,因此它可以连接所有在Windows平台上运行的数据库。由ODBC去连接特定的数据库,JDBC就只需要连接ODBC就可以了,如图14.2所示。通过ODBC可以连接到它支持的任意一种数据库,这种连接方式叫JDBC-ODBC桥,使用这种方法让Java连接到数据库的驱动程序称为JDBC-ODBC桥驱动器。

以上介绍了两种数据库连接方式,其中JDBC-ODBC桥连接比较简单,但是只能支持Windows下的数据库连接,可移植行较差,因此这种方式用的并不多。而直接使用数据库厂商驱动的方式可移植性比较好,在实际开发中用的比较广泛,下面开始针对这种方式进行讲解。

使用厂商驱动连接数据库的步骤如下:

• 到相应的数据库厂商网站下载驱动,或者从maven官网下载驱动包,复制到项目中。

• 在JDBC代码中,设定特定的驱动程序名称、URL、数据库账号和密码。

不同的驱动程序和不同的数据库,应该采用不同的驱动程序名称、URL、数据库账号和密码。常见的数据库的驱动名称和URL如下:

MySQL:驱动程序为com.mysql.jdbc.Driver,URL为jdbc:mysql://[host:port] /[database][?参数名1][=参数值1][&参数名2][=参数值2]。例如,连接本机的MySQL数据库,名称school,用户名root,密码root,代码如下:

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

String url = "jdbc:mysql://localhost:3306/school?Unicode=true&characterEncoding=UTF-8";

String user = "root";

String password = "root";

Connection conn = DriverManager.getConnection(url,user,password);

• Oracle:驱动程序为oracle.jdbc.driver.OracleDriver,URL为jdbc: oracle: thin: @ [ip]: 1521: [sid]。例如,连接本机的Oracle数据库,SID为school,用户名为scott,密码为tiger,代码如下:

String user = "scott";

String password = "tiger";

String url = "jdbc:oracle:thin:@localhost:1521:school";

Class.forName("oracle.jdbc.driver.OracleDriver");

Connection conn=DriverManager.getConnection(url,user,password);

• SQLServer:驱动程序为com.microsoft.jdbc.sqlserver.SQLServerDriver,URL为jdbc:microsoft: sqlserver://[ip]:1433;DatabaseName=[DBName]。例如,连接本机的SQL Server数据库,名称school,用户名为sa,密码为sa,代码如下:

String user = "sa";

String password = "sa";

String url = "jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=school";

Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver");

Connection conn = DriverManager.getConnection(url,user,password);

注意: 我们使用数据库能够正常操作的前提是:必须将相应的包复制到项目的classpath下面,在IDEA中,可以在项目中导入该包。

下载厂商驱动的操作非常简单,以MySQL数据库为例,可从Maven网站下载MySQL驱动包(https://mvnrepository.com/),并选择版本号为5.1.47,如图14.3、图14.4所示。下载成功后保存在本地D:\ jar路径中并解压该文件,如图14.5所示。

图14.3 打开maven官网搜索jar包 图14.4 选择jar包版本下载jar包 图14.5 jar存放的位置

14.2 JDBC常用API

JDBC相关的API存放在java.sql包中,主要包括表14.1所示的类或接口。

表14.1 常用的接口

类或接口

Driver

每个驱动程序类必须实现的接口

DriverManager

JDBC驱动管理类

Connection

与特定数据库的连接(会话)

Statement

用于执行静态SQL语句并返回其生成的结果的对象

PreparedStatement

Statement接口的一个子接口,可以对sql进行预编译

CallableStatement

Statement接口的一个子接口,可以执行存储过程

ResultSet

表示数据库结果集的数据表,通常通过执行查询数据库的语句生成

14.2.1 Driver接口

在JDBC技术中,Driver是java.sql包下面的一个接口,如果数据库厂商需要提供驱动程序,就需要实现该接口。不同的数据库的驱动路径是不一样的,MySQL的数据库驱动路径为:com.mysql.jdbc.Driver。

加载驱动程序是JDBC操作的第1步,之前已经将数据库的驱动程序讲解过了,这里直接使用,首先拿到我们数据库驱动配置到项目的classpath中,如图14.6和14.7所示。

接下来,通过程序来演示一下Java如何操作MySQL数据库。

如图14.6所示,新建一个包名字lib并把驱动包mysql-connector-java-5.1.47.jar放入lib包中。

鼠标在jar包上右键选择 “Add as Library...”,如图14.7所示,添加到“Add as Library...”之后,此jar包加载成功。

图 14.6 新建项目添加jar包 图 14.7 添加后效果

接下来,通过案例来演示加载驱动,如例14-1所示。

14-1 Demo1401.java

1 package com.aaa.p140201;

2 public class Demo1401 {

3 // 定义MySQL的数据库驱动程序

4 public static final String Driver = "com.mysql.jdbc.Driver";

5 public static void main(String[] args) {

6 // 加载数据库驱动

7 try {

8 Class.forName(Driver);

9 System.out.println("驱动加载成功");

10 } catch (ClassNotFoundException e) {

11 e.printStackTrace();

12 }

13 }

14 }

程序运行结果如下:

驱动加载成功

注意:如果以上程序能够输出“驱动加载成功”,则证明数据库驱动程序已经配置成功。如果出现错误提示:java.lang.ClassNotFoundException:com.mysql.jdbc.Driver 则问题是驱动jar出错了。需要检查是否正确引入了驱动包或者驱动名称是否写错。

14.2.2 DriverManager类

数据库驱动程序加载成功之后,如果想将JDBC的驱动程序连接到数据库,就需要使用DriverManager类中的方法来创建连接。DriverManager类中的常用的方法如表14.2所示。

表14.2 DriverManager类的常用方法

方法

说明

public static Connection getConnection(String url)

通过连接地址连接数据库

public static Connection getConnection(String url,String user,String password) throws SQLException

通过连接地址连接数据库,同时输入用户名和密码

在DriverManager类中,提供的主要操作就是得到一个数据库的连接,getConnection()方法就是取得连接对象,此方法返回的类型是Connection对象(Connection接口在第14.2.3节讲解)。表14.2中提供了两种方式来连接数据库,不管使用哪种连接,都必须提供一个数据库的连接地址。连接MySQL数据库具体示例如下:

jdbc:mysql:                        // mysql数据库服务器的IP地址:端口号/数据库名称

现在我们以school数据库为例,创建school的数据库脚本如下:

create database school;

那么,通过JDBC连接数据库的URL地址为jdbc:mysql://localhost:3306/school。

知识点拨:连接数据库的时候,如果数据库安装在本机,则数据库的连接字符串可以简写为:jdbc:mysql:///school。

接下来,通过案例来演示数据库的连接,如例14-2所示。

14-2 Demo1402.java

1 package com.aaa.p140202;

2 import java.sql.Connection;

3 import java.sql.DriverManager;

4 import java.sql.SQLException;

5

6 public class Demo1402 {

7 // MySQL的数据库驱动程序

8 public static final String DRIVER = "com.mysql.jdbc.Driver";

9 // 数据库的URL连接地址

10 public static final String URL = "jdbc:mysql://localhost:3306/school";

11 public static final String UNAME = "root";         // 数据库的账号

12 public static final String PSWD = "root";            // 数据库的密码

13 public static void main(String[] args) {

14 Connection connection = null;

15 try {

16 Class.forName(DRIVER);                         // 加载驱动

17 try {

18 // 建立连接需要写url,用户名和密码

19 connection = DriverManager.getConnection(URL, UNAME, PSWD);

15 System.out.println("数据库连接成功");

20 } catch (SQLException e) {

21 e.printStackTrace();

22 }

23 } catch (ClassNotFoundException e) {

24 e.printStackTrace();

25 }

26 }

27 }

程序运行结果如下:

数据库连接成功

例14-2中,使用DriverManager类中的getConnection(String url,String user,String password)方法获取数据库连接。由于笔者的数据库设置的有用户名和密码,所以在此时需要传入用户名和密码。如果用户名或密码错误就会抛出SQLException异常。

14.2.3 Connection接口

所有的数据库的操作都是从Connection接口开始的,该接口可以实现与特定数据库的连接。Connection接口中的常用方法如表14.3所示。

表14.3    Connection接口的常用方法

方法

描述

Statement createStatement() throws SQLException

创建一个Statement对象

PreparedStatement prepareStatement(String sql) throws SQLException

创建一个PreparedStatement类型的对象

CallableStatement prepareCall(String sql)throws SQLException

创建一个CallableStatement对象,此对象用于调用数据库的存储过程

DatabaseMetaData getMetaData() throws SQLException

得到数据库的元数据

Void setAutoCommit(boolean autoCommit) throws SQLException

设置数据库的自动提交,与事务有关

Savepoint setSavepoint() throws SQLException

设置数据库的恢复点,与事务有关

void close() throws SQLException

关闭数据库

接下来,通过案例来演示Connection接口的使用,如例14-3所示。

14-3 Demo1403.java

1 package com.aaa.p140203;

2 import java.sql.Connection;

3 import java.sql.DriverManager;

4 import java.sql.SQLException;

5

6 public class Demo1403 {

7 // MySQL的数据库驱动程序

8 public static final String DRIVER = "com.mysql.jdbc.Driver";

9 // 数据库的URL连接地址

10 public static final String URL = "jdbc:mysql://localhost:3306/school";

11 public static final String UNAME = "root";         // 数据库的账号

12 public static final String PSWD = "root1";         // 数据库的密码

13 // main方法,连接数据库测试是否连接成功

14 public static void main(String[] args) {

15 Connection connection = null;

16 try {

17 Class.forName(DRIVER);         // 把驱动加载进来,建立连接

18 // 建立连接,写上对应的数据库url和账号密码

19 connection = DriverManager.getConnection(URL, UNAME, PSWD);

20 System.out.println("数据库连接成功");

21 } catch (ClassNotFoundException e) {

22 System.out.println("加载失败");             // 如果有异常会执行该语句

23 e.printStackTrace();

24 } catch (SQLException e) {

25 System.out.println("连接数据库错误");

26 e.printStackTrace();

27 }finally {

28 try {

29 connection.close();                 // 执行结束,要关闭数据库连接

30 System.out.println("关闭数据库");

31 } catch (SQLException e) {

32 System.out.println("关闭数据库异常");

33 e.printStackTrace();

34 }

35 }

36 }

37 }

程序运行结果如下:

数据库连接成功

关闭数据库

数据操作之前需要获取数据库的连接,操作之后需要将数据库访问过程中建立的各个数据库对象按顺序进行关闭,防止系统资源的浪费。例14-3中,finally代码块里执行的Connection接口的close()方法,就是关闭连接。

在后面的代码中,很多地方都需要获取数据库的连接并关闭连接,所以将这部分代码提到一个公共的BaseDAO类中,并将这个类放到com.aaa.p14.util中,代码如下:

1 package com.aaa.p14.util;

2 import java.sql.Connection;

3 import java.sql.DriverManager;

4 import java.sql.ResultSet;

5 import java.sql.SQLException;

6

7 public class BaseDAO {

8 // 定义MySQL的数据库驱动程序

9 public static final String DRIVER = "com.mysql.jdbc.Driver";

10 // 数据库连接字符串

11 public static final String URL =

"jdbc:mysql:///school?useUnicode=true&characterEncoding=utf-8 ";

12 public static final String USER = "root"; // 数据库服务器账号

13 public static final String PSWD = "root"; // 数据库服务器密码

14 public static Connection getConnection(){ // 获取连接方法

15 Connection con = null;

16 try{

17 Class.forName(DRIVER); // 加载驱动类

18 con = DriverManager.getConnection(URL, USER, PSWD); // 获取连接

19 }catch (ClassNotFoundException e) {

20 System.out.println("加载失败"); // 如果有异常会执行

21 e.printStackTrace();

22 } catch (SQLException e) {

23 System.out.println("连接数据库错误");

24 e.printStackTrace();

25 }

26 return con;

27 }

28 public static void closeAll(Connection con){ // 关闭数据库对象方法

29 try{

30 if(con!=null){

31 con.close();

32 }

33 }catch (SQLException ex){

34 ex.printStackTrace();

35 }

36 }

37 }

接下来,修改例14-3,通过BaseDAO完成数据库的连接和关闭,如例14-4所示。

14-4 Demo1404.java

1 package com.aaa.p1402;

2 import com.aaa.p14.util.BaseDAO;

3 import java.sql.Connection;


4 public class Demo1404 {

5 public static void main(String[] args) {

6 Connection conn = BaseDAO.getConnection();

7 System.out.println("数据库连接成功");

8 BaseDAO.closeAll(conn);

9 System.out.println("关闭数据库");

10 }

11 }

程序运行结果如下:

数据库连接成功

关闭数据库

运行结果和例14-3中一致。

知识点拨: JDBC连接数据库之后添加到数据库中的数据有可能出现乱码,出现乱码的原因是我们使用的字符集和数据库的字符集不一致。所以,需要在URL中定义字符集。定义字符集的时候使用useUnicode=true&characterEncoding=utf-8,这句话的以是将国际的gb2312编码转化为Unicode编码。在数据库中可以使用中文。

14.2.4 Statement接口

Statement接口 是 Java 执行数据库操作的一个重要接口。如果需要使用这个接口就必须先跟数据库建立连接。然后可以使用这个接口向数据库发送并执行静态SQL语句(所谓静态SQL语句,是指在执行Statement接口的executeUpdate()、executeQuery()等方法时,作为参数的SQL语句的内容时固定不变的,也就是SQL语句中没有任何的参数),然后返回其生成的结果。Statement接口中定义了如表14.4所示的常用方法。

表14.4 Statement接口的常用方法

方法

描述

int executeUpdate(String sql)

执行给定的SQL语句,这可能是 INSERT 、 UPDATE或 DELETE语句

ResultSet executeQuery(String sql)

执行给定的SQL语句,该语句返回单个 ResultSet对象

void addBatch(String sql)

将给定的SQL命令添加到此 Statement对象的当前命令列表中

int[] executeBatch()

将一批命令提交到数据库以执行,并且所有命令都执行成功,返回一个更新计数的数组

void close()

关闭数据库的连接

boolean execute(String sql)

执行给定的SQL语句,这可能会返回多个结果

接下来,我们先在数据库中创建一张表,名称为students,字段如表14.5所示。

表14.5    students表中的字段

字段

类型

长度

描述

id

int

10

自动增长的主键

name

varchar

50

学生的名字

gender

varchar

4

学生的性别

age

varchar

10

学生的年龄

接着,我们通过向students表中添加一条数据来演示如何使用Statement接口,如例14-5所示。

14-5 Demo1405.java

1 package com.aaa.p140204;

2 import com.aaa.p14.util.BaseDAO;

3 import java.sql.Connection;

4 import java.sql.SQLException;

5 import java.sql.Statement;

6

7 public class Demo1405 {

8 public static void main(String[] args) {

9 Connection connection = BaseDAO.getConnection();    // 获取连接

10 Statement statement = null;

11 try {

12 statement = connection.createStatement();     // 创建Statement对象

13 // 定义sql语句

14 String sql = "insert into students(name,gender,age) values('张三','',18)";

15 int i = statement.executeUpdate(sql);         // 执行sql语句并返回结果

16 System.out.println("受影响的行数为:" + i);

17 } catch (SQLException e) {

18 e.printStackTrace();

19 }finally {

20 if(statement != null){

21 try {

22 statement.close();         // statement使用后要关闭

23 } catch (SQLException e) {

24 e.printStackTrace();

25 }

26 }

27 BaseDAO.closeAll(connection);         // 关闭Connection

28 }

29 }

30 }

程序运行结果如下:

受影响的行数为:1

例14-5中,先通过BaseDAO类的getConnection()方法获取Connection对象,然后使用Connection接口中的createStatement()方法获取Statement对象,接着通过executeUpdate(String sql)方法执行一条标准的SQL语句并返回对应的结果。Statement对象使用之后需要将它关闭,由于在后面的代码中也需要关闭该对象,所以需要修改一下公共类BaseDAO中的closeAll()方法。修改之后的CloseAll()方法如下:

public static void closeAll(Connection con, Statement stmt){

try{

if(stmt!=null){

stmt.close();

// System.out.println("关闭statement");

}

if(con!=null){

con.close();

// System.out.println("关闭数据库");

}

}catch (SQLException ex){

ex.printStackTrace();

}

}

例14-5中,SQL语句中列的值是固定的,如果列的值不固定,我们就需要动态地传入列的值。由于Statement接口中需要执行的是静态SQL语句,所以在调用executeUpdate()方法之前需要把对应的SQL语句拼接好。

接下来,重新修整例14-5的代码,如例14-6所示。

14-6 Demo1406.java

1 package com.aaa.p140204;

2 import com.aaa.p14.util.BaseDAO;

3 import java.sql.Connection;

4 import java.sql.SQLException;

5 import java.sql.Statement;

6

7 public class Demo1406 {

8 public static void main(String[] args) {

9 Connection conn = null;

10 Statement statement = null;

11 String name = "张三";

12 String gender = "";

13 Integer age = 18;

14 try {

15 conn = BaseDAO.getConnection();

16 statement = conn.createStatement();

17 // 定义sql语句并将对应的变量值拼接到sql语句中

18 StringBuilder sqlBuffer=

19 new StringBuilder("insert into students(name,gender,age) ");

20 sqlBuffer.append(" values ( ").append("'").append(name)

21 .append("',").append(gender)

22 .append(",").append(age).append(")");

23 statement = conn.createStatement();          // 实例化Statement对象

24 int i = statement.executeUpdate(sql.toString());// 执行sql语句

25 System.out.println("受影响的行数为:" + i);

26 } catch (SQLException e) {

27 e.printStackTrace();

28 }finally {

29 BaseDAO.closeAll(conn,statement); // 关闭 connection statement

30 }

31 }

32 }

程序运行结果如下:

受影响的行数为:1

例14-6中,执行SQL语句使用的是Statement接口,Statement在执行SQL语句的时候需要使用静态SQL语句,因此使用StringBuilder类来拼接SQL语句。在拼接SQL语句的时候字符串必须使用英文单引号('')引起来,否则就会报com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException异常。本例使用Statement接口来执行数据库的添加操作,如果要完成修改和删除操作,则需要使用Statement接口中的executeUpdate(String sql)方法,此时只需将SQR语句改成修改语句或者删除语句即可。

14.2.5 PreparedStatement接口

PreparedStatement接口是可以对SQL语句进行预编译处理的一个接口,该接口可用于执行动态的SQL语句。所谓动态SQL语句,指的是在SQL语句中可以提供参数,对相同的SQL语句替换参数,进而实现多次使用。因此,当一个SQL语句需要执行多次时,使用预编译语句可以减少执行时间。

由于PreparedStatement是Statement的子接口,所以PreparedStatement对象可以执行动态SQL语句,也可以执行静态的SQL语句。PreparedStatement接口的常用方法,如表14.6所示。

表14.6 PreparedStatement的常用方法

方法

描述

int executeUpdate() throws SQLException

执行设置的预处理SQL语句

ResultSet executeQuery() throws SQLException

执行数据库查询操作,返回ResultSet

void setInt(int parameterIndex,int x) throws SQLException

指定要设置的索引编号,并设置整数内容

void setString (int parameterIndex,String x)

指定要设置的索引编号,并设置字符串内容

接下来,通过案例来演示使用PreparedStatement接口插入数据,如例14-7所示。

14-7 Demo1407.java

1 package com.aaa.p140205;

2 import com.aaa.p14.util.BaseDAO;

3 import java.sql.Connection;

4 import java.sql.PreparedStatement;

5 import java.sql.SQLException;

6

7 public class Demo1407 {

8 public static void main(String[] args) {

9 Connection connection = BaseDAO.getConnection();

10 PreparedStatement pstmt = null;

11 // 定义sql 语句

12 String sql = "insert into students(name,gender,age) values(?,?,?)";

13 try {

14 pstmt = connection.prepareStatement(sql);     // sql进行预编译

15 // 对占位符按照位置进行数据填充

16 pstmt.setString(1,"李四");

17 pstmt.setString(2,"");

18 pstmt.setInt(3,20);

19 int i = pstmt.executeUpdate();             // 执行sql并返回受影响的行数

20 System.out.println("受影响的行数为:" + i);

21 } catch (SQLException e) {

22 e.printStackTrace();

23 } finally {

24 BaseDAO.closeAll(connection,pstmt);

25 }

26 }

27 }

程序运行结果如下:

受影响的行数为:1

例14-7中,使用PreparedStatement对象执行添加的操作。与Statement接口的用法不同的是,该接口先对SQL语句进行预编译,然后将需要设置值的地方使用占位符?来占位。并在调用executeUpdate()方法之前对占位符进行赋值,赋值的时候调用的是setXXX()方法(XXX代表的是要赋给的参数的具体数据类型)。

Statement接口在执行的时候使用的是静态SQL语句,而PreparedStatement接口使用占位符的方式来执行SQL语句,这种方式能够有效防止SQL注入攻击。所以推荐使用PreparedStatement接口来执行SQL语句。

知识点拨:SQL攻击又称SQL注入或者SQL注入攻击,是利用Statement的漏洞来完成的。例如,某个用户登录系统,在提交时如果在用户名文本框内输入 “李四’ or ’1’=’1”,而密码输入'xxx',那么对应的 SQL的查询语句就是 “select * from 表名 where 用户名=’李四’ or ’1’=’1’ and 密码=’xxx’  ”,这样的SQL语句会查询出数据库对应表中所有的数据。如果想要避免这种情况出现,一般采用PreparedStatement来进行SQL的预编译,这样就可以避免SQL注入攻击。

    在数据库处理中,增删改是十分常用的操作,所以要进行封装,以实现代码的复用。另外,执行SQL语句时,一般也需要设置SQL语句中的参数数据,这个方法也可以封装成通用的方法。下面将增删改方法和设置参数的方法封装到BaseDAO类中,代码如下:

package com.aaa.p14.util;

import java.sql.*;


public class BaseDAO {

// 此处省略获取连接和关闭连接的方法

// 通用设置参数的方法

public static void setParams(PreparedStatement pst,Object[] params){

if(params == null){         // 如果没有数据,则不作设置

return;

}

try {

for (int i = 0; i < params.length; i++) { // 循环设置参数

pst.setObject(i + 1, params[i]);

}

}catch (Exception ex){

ex.printStackTrace();

}

}

// 通用增删改方法

public static int executeUpdate(String sql,Object[] params){

Connection con = null;

PreparedStatement pst = null;

int res = 0;

try{

con = getConnection(); // 创建连接

pst = con.prepareStatement(sql);

setParams(pst,params); // 设置参数值

res = pst.executeUpdate(); // 执行增删改操作

}catch (Exception ex){

ex.printStackTrace();

}finally {

closeAll(con,pst,null);

}

return res;

}

}

    接下来,使用封装后的BaseDAO类来实现例14-7的功能,如例14-8所示。

14-8 Demo1408.java

1 package com.aaa.p140205;

2 import com.aaa.p14.util.BaseDAO;

3

4 public class Demo1408 {

5 public static void main(String[] args) {

6 String sql="insert into students(name,gender,age) values(?,?,?)";

7 Object[] params = {"李四","",20};

8 int i = BaseDAO.executeUpdate(sql,params);

9 System.out.println("受影响的行数为:" + i);

10 }

11 }

程序运行结果如下:

受影响的行数为:1

14.2.6 CallableStatement接口

CallableStatement接口是Statement接口的子接口, Java程序在通过JDBC调用存储过程时,需要先通过一个数据库连接创建一个CallableStatement对象,该对象包含对存储过程的调用以及执行。CallableStatement接口中的常用方法,如表14.7所示。

表14.7    CallableStatement接口常用方法

方法

描述

int getInt (int parameterIndex)

按照索引获取指定的过程的返回值

int getInt(String parameterName)

按照名称获取指定的过程的返回值

void registerOutParameter(int parameterIndex, int sqlType)

设置返回值的类型,需要指定Types类

String getString (int parameterIndex)

按照索引获取指定的过程的返回值

String getString (String  parameterName)

按照名称获取指定的过程的返回值

CallableStatement对象为所有的数据库系统提供了一种标准的形式去调用数据库中已存在的存储过程,调用存储过程的语法格式如下:

{ call 存储过程名(?, ?, ...)}             // 其中,?是参数占位符

接下来,通过案例来演示CallableStatement接口的使用,如例14-9所示。

14-9 Demo1409.java

1 package com.aaa.p140206;

2 import com.aaa.p14.util.BaseDAO;

3 import java.sql.*;

4

5 public class Demo1409 {

6 public static void main(String[] args) {

7 Connection conn = null;

8 CallableStatement callableStatement = null;

9 try {

10 conn = BaseDAO.getConnection();

11 // 调用存储过程

12 String sql = "{call pro_getNameById(?,?)}";

13 callableStatement = conn.prepareCall(sql); // 获取存储过程执行对象

14 callableStatement.setInt(1,1); // 对占位符进行数据填充

15 // 注册out参数,第1个参数代表的是参数的位置,第2个代表的是参数的类型

16 callableStatement.registerOutParameter(2,Types.VARCHAR);

17 callableStatement.execute(); // 执行存储过程

18 // 根据位置获取输出参数,输出参数的位置为2

19 String sname = callableStatement.getString(2);

20 System.out.println(sname);

21 } catch (Exception e) {

22 e.printStackTrace();

23 }finally {

24 BaseDAO.closeAll(conn,callableStatement);

25 }

26 }

27 }

程序运行结果如下:

张三

例14-9中的存储过程的内容如下:

drop procedure if EXISTS pro_getNameById;

create PROCEDURE pro_getNameById(sid int,out sname varchar(50))

BEGIN

select `name` into sname from students where id=sid;

end;

例14-9中,使用Connection接口中的prepareCall ()方法获取CallableStatement对象,并对SQL语句进行预编译。然后,对SQL语句中的占位符进行赋值,并注册参数的类型,最后调用CallableStatement接口中的execute()方法执行存储过程。

14.2.7 ResultSet接口

ResultSet接口用于存储查询结果的对象在SQL中,select语句可以查询数据库中的结果,对于查询的结果都需要使用ResultSet进行接收,并展示查询结果,如图14.8所示。

图14.8 查询过程

在前文已经讲解过数据库的新增操作,如果现在进行数据库查询操作,可以使用PreparedStatement接口中的executeQuery()方法完成,该方法返回值类型是ResultSet的对象,里面存放SQL的查询结果。ResultSet接口常用的操作方法,如表14.7所示。

表14.7 ResultSet接口的常用操作方法

命令

描述

int getInt(int columnIndex) throws SQLException

以整数形式按列的编号取得指定列的内容

int getInt(String columnName) throws SQLExcepption

以整数形式取得指定列的内容

float getFloat(int colommnIndex) throws SQLExceptin

以浮点数的形式按列的编号取得指定列的内容

Float getFloat(String columnName) throws SQLException

以浮点数的形式取得指定列的内容

String getString(int columnIndex) throws SQLEception

以字符串的形式按列的编号取得指定列的内容

String getString(String columnName) throws SQLException

以字符串的形式取得指定列的内容

Date getDate(int columnIndex) throws SQLException

以Date的形式按列的编号取得指定列的内容

Date getDate(String columnName) throws SQLException

以Date的形式取得指定列的内容

ResultSet对象使用完成之后需要关闭,关闭的方式和Connection接口、Statement接口一样。这段代码在后面也会使用,所以将其提取到公共代码BaseDAO类的closeAll()方法中,相关代码如下:

public static void closeAll(Connection con, Statement stmt,ResultSet resultSet){

try{

if(resultSet!=null){

resultSet.close();

System.out.println("关闭resultSet");

}

// 此处省略ConnectionStatement的关闭方法

}catch (SQLException ex){

ex.printStackTrace();

}

}

接下来,通过案例来演示ResultSet接口的使用,如例14-10所示。

14-10 Demo1410.java

1 package com.aaa.p140207;

2 import com.aaa.p14.util.BaseDAO;

3 import java.sql.*;

4

5 public class Demo1410 {

6 public static void main(String[] args) {

7 Connection conn = null;

8 PreparedStatement statement = null;

9 ResultSet resultSet = null;

10 try {

11 conn = BaseDAO.getConnection();

12 String sql = "select id,name,gender,age from students";

13 statement = conn.prepareStatement(sql);

14 // 执行数据库查询操作,并实例化ResultSet对象

15 resultSet = statement.executeQuery();

16 // 拿到实例化对象之后对结果进行打印

17 while (resultSet.next()) { // 指针下移

18 int id = resultSet.getInt("id"); // 获取id内容

19 String name = resultSet.getString("name"); // 获取name内容

20 String gender = resultSet.getString("gender");// 获取性别内容

21 int age = resultSet.getInt("age"); // 获取年龄内容

22 System.out.print("获取到的id:" + id);

23 System.out.print("获取到的name:" + name);

24 System.out.print("获取到的gender:" + gender);

25 System.out.print("获取到的age:" + age);

26 System.out.println(); // 一次循环换行

27 }

28 } catch (Exception e) {

29 e.printStackTrace();

30 } finally {

31 BaseDAO.closeAll(conn, statement, resultSet);

32 }

33 }

34 }

程序执行结果如下:

获取到的id:1获取到的name:张三获取到的gender:男获取到的age:18

获取到的id:2获取到的name:李四获取到的gender:男获取到的age:20

例14-10中,执行查询的时候调用的是PreparedStatement接口的executeQuery()方法,并将查询结果返回给ResultSet接口。ResultSet接口的next()方法的作用是将光标从当前位置向下移动一行。 光标最初位于第1行之前,第1次调用next()方法使第1行成为当前行, 第2次调用next()方法使第2行成为当前行,依此类推。通过循环获取所有数据行即可将数据库中的所有的数据获取出来,当所有的行循环完之后,next()方法会返回false,从而退出循环。

14.3 JDBC事务

在实际开发中,数据的完整性、一致性、安全性等问题,往往是至关重要的。为此,数据库领域提出了事务的概念,将单个逻辑工作单元执行的一系列SQL语句打包,要么完全地执行,要么完全地不执行。这样,可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源,进而简化错误恢复并使应用程序更加可靠。JDBC是一种可用于执行SQL语句的Java API,是连接数据库和Java应用程序的纽带。那么,JDBC又是怎样处理事务的呢,本节将展开讲解。

14.3.1 事务的概念

事务是构成单一逻辑工作单元的操作集合,它由一组SQL语句构成。事务是为了解决数据安全操作提出的,事务控制实际上就是控制数据的安全访问。例如,银行转帐业务中,账户A要将自己账户上的1000元转到账户B。这样,账户A余额首先要减去1000元,然后账户B要增加1000元。假如在中间网络出现了问题,账户A减去1000元已经结束,账户B因为网络中断而操作失败,那么整个业务失败,给客户造成损失。于是,必须做出控制,要求账户A转帐业务撤销,这才能保证业务的正确性,完成这个操走就需要事务,将账户A资金减少和账户B资金打包到一个事务里面,要么全部执行成功,要么操作全部撤销,这样就保持了数据的安全性。一般地,JDBC事务具有如下4个特征(这4个特性也被程为ACID特征):

• 原子性(Atomicity):事务最小的逻辑工作单元,是不可再分割的单元,要么全部成功,要么全部失败。

• 一致性(Consistency):事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。

• 隔离性(Isolation):并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。例如,多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。

• 持久性(Durability):事务一旦提交,其对数据库的更新就是持久的,任何事务或系统故障都不会导致数据丢失。

注意:原子性和一致性的的侧重点不同:原子性关注状态,要么全部成功,要么全部失败,不存在部分成功的状态;一致性关注数据的可见性,中间状态的数据对外部不可见,只有最初状态和最终状态的数据对外可见。

14.3.2 JDBC对事务的支持

在JDBC中处理事务,都是通过Connection对象完成的。同一事务中所有的操作,都必须使用同一个Connection对象。Connection中关于事务的方法如下:

• setAutoCommit(boolean):是否设置自动开启事务,如果为true(默认值为true)则表示自动提交事务,也就是说执行的每条SQL语句都是一个单独的事务,如果为false,则表示手动开启事务。

• commit():提交事务,所有存放到内存中的SQL语句全部提交。

• rollback():回滚事务,内存中的SQL语句如果有一条出现异常就会全部取消提交。

代码格式如下:

// 省略获取 Connection对象的方法

try{

conn.setAutoCommit(false);        // 取消自动开启事务

......

conn.commit();                    // 提交事务

} catchSQLException e {

conn.rollback();                // 回滚事务

}

14.3.3 使用事务实现批量更新

先创建一个银行账户表(Account),如表14.9所示。

表14.9 Account表结构

字段

类型

描述

id

int

自动增长的主键

name

varchar

用户姓名

money

int

账户金额

接下来,通过案例来演示如何使用JDBC操作事务,模拟银行用户张三向用户李四转账50元,如例14-11所示。

14-11 Demo1411.java

1 package com.aaa.p140303;

2 import com.aaa.p14.util.BaseDAO;

3 import java.sql.Connection;

4 import java.sql.DriverManager;

5 import java.sql.PreparedStatement;

6 import java.sql.SQLException;

7

8 public class Demo1411 {

9 public static void main(String[] args) {

10 Connection conn = null;

11 PreparedStatement preparedStatement = null;

12 try {

13 conn = BaseDAO.getConnection();

14 // Mysql默认的是自动提交事务,在这里需要改为手动提交事务

15 conn.setAutoCommit(false);

16 // 张三的账户的钱需要减50

17 String sql1 = "update students set money=money-? where name=?";

18 // 李四的账户的钱需要加50

19 String sql2 = "update students set money=money+? where name=?";

20 preparedStatement = conn.prepareStatement(sql1);     // sql1进行预编译

21 preparedStatement.setInt(1,50);     // sql1需要减去的金额

22 preparedStatement.setString(2,"张三");     // sql1中的name的值

23 preparedStatement.executeUpdate();     // 执行sql1

24 System.out.println(1/0);     // 模拟异常

25 preparedStatement = conn.prepareStatement(sql2);     // sql2进行预编译

26 preparedStatement.setInt(1,50);     // sql2需要添加的金额

27 preparedStatement.setString(2,"李四");     // sql2中的name的值

28 preparedStatement.executeUpdate();     // 执行sql2

29 conn.commit();     // 提交事务

30 } catch (Exception e) {

31 System.out.println("出现异常。");

32 }finally {

33 BaseDAO.closeAll(conn,preparedStatement);              // 关闭连接

34 }

35 }

36 }

程序执行结果如下:

出现异常。

执行程序之前,数据库中的数据如图14.9所示。执行程序之后数据库中的数据如图14.10所示。

例14-11中,模拟了一次银行转账。先设置事务的提交方式为非自动提交,然后定义两条SQL语句并使用PreparedStatement对其进行预编译。预编译之后分别对占位符进行赋值,第1条SQL语句是将名字为“张三”的账户的金额减少50,第2条SQL语句是将名字为“李四”的账户金额增加50。占位符赋值之后,执行SQL语句并提交事务。因为在,1条SQL语句执行之后进行了一个数学运算“1/0”,所以会抛出异常。一旦抛出异常,就会执行事务的回滚,使所有的SQL语句的执行全部取消。

例14-11中有异常出现,所以所有的SQL全部不执行,如果将第24行代码注释掉,程序没有异常,两条SQL语句会全部执行,模拟成功,执行后数据库中的数据如图14.11所示。

图14.9 执行前数据库中的数据 图14.10 执行后数据库中的数据 图14.11 无异常是执行后数据库中的数据

14.4 DAO模式

DAO(Data Access Objects)即数据存取对象,是关联业务逻辑和持久化数据的操作,可以实现对持久化数据的访问。它最大的特点是对数据库的操作都做了封装。DAO 模式提供了访问关系型数据库系统所需的接口,将数据访问层和业务层进行了分离,并对业务层的调用提供了对应的数据访问接口。本质上说,DAO模式是访问数据库的一套固定方式。

14.4.1 元数据

JDK1.3以后,JDBC技术加入了元数据的功能,有了元数据就可以很方便的获取数据库相关的信息。开发者可以通过这项功能,更加得心应手地操作数据库,同时也可以开发比以前更加自动化的程序。在JDBC中,经常被使用的元数据接口有DatabaseMeataData(数据库元数据接口)和ResultSetMetaData(结果集元数据接口)。

14.4.2 DatabaseMetaData接口

DatabaseMetaData接口可以获取整个数据库的综合信息,如数据库产品的名称等内容,该接口对象需要通过数据库连接对象获取。

接下来,通过案例来演示DatabaseMetaData接口的使用方法,如例14-12所示。

14-12 Demo1412.java

1 package com.aaa.p140402;

2 import java.sql.Connection;

3 import java.sql.DatabaseMetaData;

4 import java.sql.SQLException;

5 import com.aaa.p14.util.BaseDAO;

6

7 public class Demo1412 {

8 // main方法

9 public static void main(String[] args) {

10     try {

11 // 使用BaseDAO获取数据库连接对象

12 Connection connection = BaseDAO.getConnection();

13 // 使用连接对象获取数据库元数据对象

14 DatabaseMetaData data = connection.getMetaData();

15 // 获取数据库产品名称

16 String productName = data.getDatabaseProductName();

17 // 获取驱动名称

18 String driverName = data.getDriverName();

19 // 获取驱动版本号

20 String productVersion = data.getDatabaseProductVersion();

21 // 获取默认隔离级别

22 int isolation = data.getDefaultTransactionIsolation();

23 System.out.println("数据库产品名称:" + productName);

24 System.out.println("数据库驱动名称:" + driverName);

25 System.out.println("驱动版本:" + productVersion);

26 System.out.println("隔离级别:" + isolation);

27 // 使用BaseDAO关闭数据库连接

28 BaseDAO.closeAll(connection, null, null);

29 } catch (SQLException e) {

30 e.printStackTrace();

31 }

32 }

33 }

程序运行结果如下:

数据库产品名称:MySQL

数据库驱动名称:MySQL Connector Java

驱动版本:5.7.22-log

隔离级别:2

例14-12中,先通过BaseDAO获取了数据库连接对象Connection。接着,使用数据库连接对象获取了数据库的元数据对象DatabaseMetaData。然后,使用数据库元数据对象分别获取了数据库产品名称、数据库驱动名称、驱动版本、隔离级别等数据库信息,并将这些信息打印输出。最后关闭数据库连接对象。

14.4.3 ResultSetMetaData接口

ResultSetMetaData接口可以获取使用JDBC查询到的ResultSet(结果集)对象中列的相关信息。因为结果集是从数据库中查询到的一个二维表,通过ResultSetMetaData接口可以获得这个二维表的列的数量、列的类型、列的名称等信息。ResultSetMetaData接口常用的方法如表14.10所示。

表14.10 ResultSetMetaData接口的常用方法

方法

描述

int getColumnCount()

获得本次查询结果的列数

String getColumnName(int column)

获得本次查询中指定列的列名

String getColumnTypeName(int column)

获得指定列的特定的类型名称

int getColumnDisplaySize(int column)

获取指定列的最大标准宽度,以字符为单位

String getTableName(int column)

获取指定列所属的表名称

接下来,通过案例来演示ResultSetMetaData接口相关方法的使用,如例14-13所示。

14-13 Demo1413.java

1 package com.aaa.p140403;

2 import java.sql.Connection;

3 import java.sql.PreparedStatement;

4 import java.sql.ResultSet;

5 import java.sql.ResultSetMetaData;

6 import com.aaa.p14.util.BaseDAO;

7

8 public class Demo1413 {

9 public static void main(String[] args) {

10     Connection con = null;         // 定义连接对象变量     

11     PreparedStatement pst = null;         // 定义预编译命令执行对象变量

12     ResultSet rs = null;         // 定义结果集对象变量

13     // 定义查询的sql语句

14     String sql = " select id, name, gender, age from students ";

15     try {

16         con = BaseDAO.getConnection();         // 使用BaseDAO获取连接对象

17         pst = con.prepareStatement(sql);         // 创建预编译命令执行对象

18         rs = pst.executeQuery();         // 执行查询,并返回结果集对象

19         // 通过结果集对象获取ResultSetMetaData(结果集元数据对象)

20         ResultSetMetaData rsmd = rs.getMetaData();

21         // 通过ResultSetMetaData获取结果集的列的个数

22         int colCount = rsmd.getColumnCount();

23         // 输出当前结果的列数

24         System.out.println("当前查询结果集的列数: " + colCount);

25         // 通过循环获取结果集中每个列的名字和列的类型信息

26         for(int i=1; i <= colCount; i++) {

27             String colName = rsmd.getColumnName(i); // 获取当前列的列名

28             String colTypeName = rsmd.getColumnTypeName(i); // 获取当前列的类型名

29             System.out.println("" + i + " 列名:"

30                              + colName + "\t 类型:"

31                              + colTypeName); // 输出列的类型和名称

32         }

33     }catch(Exception ex) {

34         ex.printStackTrace();

35     }finally {

36         BaseDAO.closeAll(con, pst, rs); // 关闭连接

37     }

38 }

39 }

程序运行结果如下:

当前查询结果集的列数: 4

1 列名:id     类型:INT

2 列名:name     类型:VARCHAR

3 列名:gender     类型:CHAR

4 列名:age     类型:INT

例14-13中,首先使用BaseDAO获取了数据库连接对象Connection。接着,使用数据库连接对象创建PreparedStatement对象,并绑定查询的SQL语句。然后,执行查询操作并返回ResultSet结果集对象。再接着,使用结果集对象获取ResultSetMetaData结果集元数据对象。通过ResultSetMetaData先获取了结果集的列数,并将获取的列数打印输出。然后,使用循环的方式逐一访问结果集的每个列,并将列名和列的类型名称打印输出。最后,在finally代码块中使用BaseDAO的closeAll()方法关闭数据库对象。

通过ResultSetMetaData接口,可以实现通用的查询方法。将通用查询方法定义在BaseDAO工具类中,示例如下:

public class BaseDAO {

// 此处省略获取连接,关闭连接,设置参数,通用增删改的方法

...

/**

    * 通用查询方法

    * @param sql 要执行的查询语句

    * @param params 查询语句中要用到的参数数据

    * @return 返回List<Map<String,Object>>封装的查询结果

*/

public static List<Map<String,Object>> executeQuery(String sql,Object[] params){

// 定义行的集合,存储结果数据

List<Map<String,Object>> rows = new ArrayList<Map<String,Object>>();

Connection con = null;             // 连接对象

PreparedStatement pst = null;             // 命令执行对象

ResultSet rs = null;             // 结果集对象

try{

con = getConnection();             // 获取连接

pst = con.prepareStatement(sql);             // 获取命令执行对象,绑定sql

setParams(pst,params);             // 设置参数

rs = pst.executeQuery();             // 执行查询

// 通过结果集对象获取结果集的结构对象

ResultSetMetaData rsmd = rs.getMetaData();

// 通过结果集的结构对象,获取结果的列数

int colCount = rsmd.getColumnCount();

// 遍历每一行数据,最终存入List<Map>

while (rs.next()){                             // 读取一行

// 定义Map存储当前行的各个列数据

Map<String,Object> cols = new HashMap<String,Object>();

// 循环当前行的各个列,一个一个放入Map

// 注意: 数据库的索引都是从1开始的,java数组的索引从0开始

for(int i = 1;i <= colCount;i++){

// 获取当前列的列名:结构信息 rsmd来获取

String colName = rsmd.getColumnName(i);

// 获取当前列的数据:内容信息 rs来获取

Object colVal = rs.getObject(i);

// 以键值对方式存入Map

cols.put(colName,colVal);

}

// 将存储了当前行数据的Map存入rows集合(List<Map>)

rows.add(cols);

}

}catch (Exception ex){

ex.printStackTrace();

}finally {

closeAll(con,pst,rs);

}

return rows;

}

}

    接下来,使用BaseDAO封装的通用查询方法,查询学生表数据,如例14-14所示。

14-14 Demo1414.java

1 package com.aaa.p140403;

2 import com.aaa.p14.util.BaseDAO;

3

4 public class Demo1414 {

5 public static void main(String[] args) {

6     // 定义查询的sql语句

7     String sql = " select id, name, gender, age from students ";

8     // 使用BaseDAO通用查询方法,查询学生数据

9     List<Map<String,Object>> studentList = BaseDAO.executeQuery(sql, null);

10     // 遍历查询的数据

11     for(Map<String,Object> map : studentList) {

12         System.out.println(map);

13     }

14 }

15 }

程序运行结果如下:

{gender=, name=张三, id=1, age=20}

例14-14中,先定义了查询学生的SQL语句,然后调用BaseDAO的通用查询方法,执行该SQL语句。因为该SQL语句没有参数,所以SQL参数数据传入null值。执行完成之后接受返回的结果,最后遍历结果并输出。

14.4.4 使用DAO模式

DAO是一个数据访问接口,位于业务层和数据库资源中间,用于提供访问数据库的相关方法。

在Java企业级开发模式中,为了建立一个健壮的Java企业级应用,应该将所有对数据源的访问操作抽象封装在一个公共API中。从程序设计的角度来说,就是建立一个接口,接口中定义了此应用程序中将会用到的访问数据库的方法,主要是增、删、改、查,即常说的CRUD方法。然后,编写一个单独的类来实现这个接口,以完成具体的数据库操作。在应用程序中,当需要和数据源进行交互的时候,都需要通过访问这个接口和这个接口的实现类来完成,这就是DAO模式的设计原理。DAO模式通过分层设计将业务逻辑层和数据访问层分开,这样使程序的功能分工明确,降低了耦合性并提高了重用性。

在具体使用DAO模式进行数据库处理的时候,一般要遵循如下步骤:

• 创建用于简化数据库操作的辅助工具类BaseDAO,在BaseDAO中封装了通用的增删改查方法。

• 创建对应某个表的模型实体类(Entity),用于封装和传递实体数据。

• 创建对应某个表的DAO接口,并在接口中定义处理数据的相关方法。

• 创建DAO接口的实现类,并实现接口中定义的方法。

• 创建测试类,使用DAO模式对数据库进行各种操作。

在上面的步骤中,创建的DAO模式的核心类有DAO接口和DAO接口的实现类,它们之间的关系如图14.12所示。

在图14.12中,DAO实现类实现了DAO接口,那么就需要具体实现DAO接口中定义的针对数据操作的所有接口方法。同时,在DAO实现类中导入BaseDAO工具类,这样在进行具体的数据处理的时候,就能够直接使用BaseDAO中封装好的通用方法,从而更方便的完成数据的相关处理。

接下来,通过案例来演示DAO模式的使用,如例14-15所示。

14-15 以前面小节定义的学生表为例,通过DAO模式对学生信息进行增删改查操作。实现DAO模式的代码文件主要有:BaseDAO.java、Student.java、IStudentDAO.java、StudentDAOImpl.java和DAOTest.java。其中,BaseDAO用之前封装好的代码,其他几个类的代码按下面的定义创建,那么对应的DAO模式的代码结构如图14.13所示。

图14.12 DAO模式核心类结构图 图14.13 针对学生表操作的DAO模式代码结构图

下面给出具体的实现步骤:

步骤1:在项目src目录下新建包com.aaa.p140404.entity,并在该包下创建学生实体类文件Student.java,代码如下:

1 package com.aaa.p140404.entity;

2

3 public class Student {

4     private Integer id;                                                 // 编号

5     private String name;                                                 // 姓名

6     private String gender;                                                 // 性别

7     private Integer age;                                                 // 年龄

8     public Student(String name, String gender, Integer age) {             // 构造函数

9         this.name = name;

10         this.gender = gender;

11         this.age = age;

12     }

13     public Student(Integer id,String name,String gender,Integer age) {// 构造函数

14         this.id = id;

15         this.name = name;

16         this.gender = gender;

17         this.age = age;

18     }

19     // 此处省略 getter & setter

20     // 此处省略 toString()

21 }

步骤2:在项目src目录下新建包com.aaa.p140404.dao,并在该包下创建学生DAO接口文件IStudentDAO.java,代码如下:

1 package com.aaa.p140404.dao;

2 import java.util.List;

3 import java.util.Map;

4 import com.aaa.p140404.entity.Student;

5

6 public interface IStudentDAO {                 // DAO接口

7     List<Map<String,Object>> findAll();         // 查询所有数据

8     int doAdd(Student student);                 // 添加数据

9     int doUpdate(Student student);                 // 修改数据

10     int doDelete(Integer id);                     // 删除数据

11 }

步骤3:在项目src目录下新建包com.aaa.p140404.dao.impl,并在该包下创建学生DAO接口实现类文件StudentDAOImpl.java,代码如下:

1 package com.aaa.p140404.dao.impl;

2 import java.util.List;

3 import java.util.Map;

4 import com.aaa.p14.util.BaseDAO;

5 import com.aaa.p140404.dao.IStudentDAO;

6 import com.aaa.p140404.entity.Student;

7

8 public class StudentDAOImpl implements IStudentDAO{

9     @Override

10     public List<Map<String, Object>> findAll() {

11         // 定义查询的sql语句

12         String sql = "select id, name, gender, age from students ";

13         // 调用BaseDAO中的通用查询方法,完成查询操作

14         return BaseDAO.executeQuery(sql,null);

15     }

16     @Override

17     public int doAdd(Student student) {

18         // 定义添加的sql语句

19         String sql = "insert into students "

20                 + " (name,gender,age)"

21                 + " values"

22                 + " (?,?,?)";

23         // 定义插入数据需要的参数

24         Object[] params = {

25                 student.getName(),

26                 student.getGender(),

27                 student.getAge()

28         };

29         // 调用BaseDAO中的通用方法,完成插入操作

30         return BaseDAO.executeUpdate(sql,params);

31     }

32     @Override

33     public int doUpdate(Student student) {

34         // 定义修改的sql语句

35         String sql = "update students"

36                 + " set name = ?,"

37                 + " gender = ?,"

38                 + " age = ?"

39                 + " where id = ? ";

40         // 定义修改使用的参数

41         Object[] params = {

42                 student.getName(),

43                 student.getGender(),

44                 student.getAge(),

45                 student.getId()

46         };

47         // 调用BaseDAO中的通用增删改方法,完成修改方法

48         return BaseDAO.executeUpdate(sql,params);

49     }

50     @Override

51     public int doDelete(Integer id) {

52         String sql = "delete from students where id = ? "; // 定义删除的sql语句        

53         Object[] params = {id}; // 定义删除使用的参数

54         return BaseDAO.executeUpdate(sql,params); // 完成删除操作

55     }

56 }

步骤4: 在项目src目录下新建包com.aaa.p140404.test,并在该包下创建测试类文件Demo1415.java,代码如下:

1 package com.aaa.p140404.test;

2 import com.aaa.p140404.dao.IStudentDAO;

3 import com.aaa.p140404.dao.impl.StudentDAOImpl;

4 import com.aaa.p140404.entity.Student;

5

6 public class Demo1415 {

7     public static void main(String[] args) {

8         // 创建DAO对象,实现增删改查操作

9         IStudentDAO studentDAO = new StudentDAOImpl();

10         // 创建学生对象,用于添加数据

11         Student s1 = new Student("张三","",20);

12         // 添加学生,并返回插入的记录数

13         int count = studentDAO.doAdd(s1);

14         // 输出记录数

15         System.out.println("插入的记录数: " + count);

16         // 查询添加后的学生表中的数据,并打印输出

17         System.out.println("插入后表中数据: " + studentDAO.findAll());

18         // 创建学生对象,用于修改数据,将上面添加到数据库的张三改为李四

19         Student s2 = new Student(1,"李四","",18);

20         // 修改学生, 并返回修改的记录数

21         count = studentDAO.doUpdate(s2);

22         // 输出记录数

23         System.out.println("修改的记录数: " + count);

24         // 查询修改后的学生表中的数据,并打印输出

25         System.out.println("修改后表中数据: " + studentDAO.findAll());

26         // 删除学生,返回删除的学生记录数

27         count = studentDAO.doDelete(1);

28         // 输出记录数

29         System.out.println("删除的学生记录数: " + count);

30         // 查询删除学生后表中的数据,并打印输出

31         System.out.println("删除后表中数据: " + studentDAO.findAll());

32     }

33 }

程序运行结果如下:

插入的记录数: 1

插入后表中数据: [{gender=, name=张三, id=1, age=20}]

修改的记录数: 1

修改后表中数据: [{gender=, name=李四, id=1, age=18}]

删除的学生记录数: 1

删除后表中数据: []

例14-15中, Student.java定义了学生实体类,实体类中的属性和数据库中学生表的列一一对应,实体类的作用主要是用于封装学生的数据。IStudentDAO.java中定义了DAO接口,在接口中声明了用于处理学生数据的增删改查的抽象方法。StudentDAOImpl.java中定义了实现类,实现了DAO接口中定义的所有抽象方法,并使用BaseDAO中定义的通用方法完成了数据库的增删改查操作。最后在Demo1415.java中,调用StudentDAO完成了针对学生数据的增删改查处理,并打印输出对应的处理结果。

知识点拨:对于DAO模式的使用,新手不太容易掌握,会感觉代码结构复杂,这是因为大家刚开始接触,使用还不够熟练。DAO模式其实是一个代码“套路”,它的规则是固定的,大家只要按照步骤一步一步去创建相应的类即可完成DAO模式的搭建,然后通过反复练习,相信很快就能掌握。另外对于BaseDAO的使用,建议大家参照案例,先学会直接使用这个工具类,等到能熟练掌握后,再深入理解它的实现方式。

14.5 数据库连接池技术

在开发基于数据库的程序时,传统模式访问数据库基本按照这几个步骤进行:创建数据库连接、进行SQL操作、断开数据库连接。这种开发模式存在一定的问题,因为JDBC的数据库连接对象使用 DriverManager 来获取,每次建立数据库连接的时候都要将连接对象加载到内存中,然后再验证用户名和密码,这个过程得花费0.05s~1s的时间,用完之后还需要把连接释放掉,非常消耗服务器资源。若同时有几十万人甚至上百万人在线,那么频繁地创建数据库连接将占用很多的系统资源,严重时会造成服务器的崩溃。

为了解决传统开发中的数据库连接问题,可以采用数据库连接池技术。数据库连接池技术负责分配、管理和释放数据库连接,它允许程序重复使用一个现有的数据库连接,而不是每次新建一个。数据库连接池技术在初始化时会创建一定数量的数据库连接对象并放到连接池中,这些数据库连接的数量可以通过最小数据库连接数来设定。无论连接池中的连接是否被使用,连接池都将一直保证至少有一定数量的数据库连接。另外,连接池技术通过最大数据库连接数量来限定连接池能创建的最大连接数,当应用程序向连接池请求的连接数超过连接池限定的最大连接数时,这些请求将被加入到等待队列中,直到连接池中有空闲连接时,再分配给这些请求。

目前,使用比较普遍的两种开源的数据库连接池库组件是DBCP和C3P0,下面详细讲解这两个连接池。

14.5.1 DBCP数据库连接池技术

DBCP 数据库连接池是 Apache 软件基金组织下的开源连接池组件,该连接池组件依赖于该组织下的另一个开源系统common-pool。所以,在使用DBCP连接池的时候,需要在程序中引用如下 jar 文件:commons-dbcp2-2.7.0.jar(连接池的实现库)、commons-pool2-2.7.0.jar(连接池依赖库)和commons-logging-1.2.jar(日志工具库)。关于如何在项目中引入jar文件,请参考第14.2节。

另外,使用DBCP连接池除了需要引入相关的jar文件外,还需要创建一个properties文件进行连接池参数的配置,具体的参数配置内容如表14.11所示。

表14.11 DBCP连接池配置参数

连接池配置参数

描述

maxActive

连接池支持的最大连接数,最多能支持的连接数

maxIdle

连接池中最多可空闲的连接数量,其余空闲的会被释放,来保证性能

minIdle

释放连接时,最少保留的空闲的连接数量

initialSize

数据库初始化时,创建的连接数量

maxWait

连接池中连接用完时,新的请求等待时间,毫秒

driverClassName

JDBC数据库驱动类的类名

url

连接路径

username

数据库用户名

password

数据库密码

connectionProperties

连接参数

defaultAutoCommit

设置事务的提交状态,默认为true

    接下来,通过案例来演示DBCP连接池的使用,如例14-16所示。

14-16 以前面小节创建的数据库school为例,使用DBCP连接池实现对数据库的访问。在该例中需要创建DBCP连接池配置文件dbcp.properties和代码文件Demo1416.java。

步骤1:在项目src目录下新建包com.aaa.p140501,并在该包下创建DBCP连接池配置文件dbcp.properties,代码如下:

1 # 数据库驱动类

2 driverClassName=com.mysql.jdbc.Driver

3 # 数据库连接地址

4 url=jdbc:mysql://localhost:3306/school

5 # 数据库服务器用户名

6 username=root

7 # 数据库服务器密码

8 password=root

9 # 连接池初始连接数

10 initialSize=10

11 # 连接池最大连接数

12 maxActive=50

13 # 连接池最大空闲连接数

14 maxIdel=20

15 # 连接池最小空闲连接数

16 minIdle=5

17 # 连接池最大等待时间(毫秒)

18 maxWait=60000

19 # 数据库连接参数

20 connectionProperties=useUnicode=true&characterEncoding=utf-8&useSSL=false

21 # 连接池事务提交状态

22 defaultAutoCommit=true

步骤2:在包com.aaa.p140501下创建代码文件Demo1416.java,实现通过DBCP连接池访问数据库,代码如下:

1 package com.aaa.p140501;

2 import java.io.InputStream;

3 import java.sql.Connection;

4 import java.sql.PreparedStatement;

5 import java.sql.ResultSet;

6 import java.util.Properties;

7 import javax.sql.DataSource;

8 import org.apache.commons.dbcp2.BasicDataSourceFactory;

9

10 // DBCP连接池测试类

11 public class Demo1416 {

12     public static void main(String[] args) throws Exception {

13         // 加载dbcp.properties配置文件,返回输入流对象

14         InputStream is = DBCPTest.class.

15 getResourceAsStream("/com/aaa/p1405/dbcp.properties");

16         // 创建Properties对象

17         Properties p = new Properties();

18         // properites文件中的数据加载到Properties对象中

19         p.load(is);

20         // 创建DBCP连接池数据源对象

21         DataSource ds = BasicDataSourceFactory.createDataSource(p);

22         // 从连接池中获取数据库连接

23         Connection con = ds.getConnection();

24         // 定义插入的sql

25         String sql = "insert into students " + " (name, gender, age)"

26                   + " values" + " ('张三','',20) ";

27         // 创建预编译命令执行对象, 绑定插入sql语句

28         PreparedStatement pst = con.prepareStatement(sql);

29         // 执行插入操作

30         int count = pst.executeUpdate();

31         // 输出插入的记录数

32         System.out.println("插入的记录数是: " + count);

33         // 定义查询的sql语句

34         sql = "select id, name, gender, age from students ";

35         // 创建预编译命令执行对象,绑定查询sql语句

36         pst = con.prepareStatement(sql);

37         // 执行查询并返回结果集对象

38         ResultSet rs = pst.executeQuery();

39         System.out.println("查询的结果是:");

40         // 通过结果集对象循环遍历查询的表数据

41         while(rs.next()) {

42             System.out.println(rs.getInt(1) + " " + rs.getString(2) +" " +

43                              rs.getString(3) + " " + rs.getInt(4));

44         }

45         // 关闭数据库对象

46         if(rs != null) {

47             rs.close();

48         }

49         if(pst != null) {

50             pst.close();

51         }

52         if(con != null) {

53             con.close();                     // 将连接放回连接池

54         }

55     }

56 }

程序运行结果如下:

插入的记录数是: 1

查询的结果是:

1 张三 20

例14-16中,首先需要在程序中引入DBCP连接池需要的jar文件。然后,创建一个properties配置文件dbcp.properties,在该文件中配置了DBCP连接池需要使用的相关参数。接着,创建了一个测试类,在测试类的main方法中,先通过文件输入流的方式,将properties文件加载到Properties对象中,从而获取了properties文件中配置的DBCP连接池的所有参数。接着,使用DBCP连接池组件中的工厂类BasicDataSourceFactory创建连接池数据源对象dataSource。然后,使用dataSource从连接池中获取一个Connection连接对象。接着,通过连接对象创建PreparedStatement预编译命令执行对象,并绑定插入的SQL语句。接着,使用PreparedStatement对象执行插入操作,向数据库插入记录,并将插入的记录数打印输出。之后,再次通过连接对象创建一个新的PreparedStatement对象,并绑定查询的SQL语句。接着,通过PreparedStatement对象执行查询操作,并返回一个ResultSet结果集对象。然后,使用结果集对象通过循环的方式将查询到的数据逐条输出。最后,依次关闭数据库对象,需要注意的是,调用连接对象的close()方法时,连接并没有被销毁而是被放回到连接池中了,下次需要使用时可以直接使用。

14.5.2 C3P0数据库连接池技术

C3P0是一个开源的数据库连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前,使用C3P0的开源项目有Hibernate、Spring等。在使用C3P0连接池的时候,需要引入jar文件c3p0-0.9.5.2.jar和mchange-commons-java-0.2.11.jar。引入jar文件的方式参考第14.2节,这里不再赘述。除此之外,还需要在项目的src根目录下创建一个C3P0连接池需要的xml配置文件,这个xml配置文件的名字必须要定义为c3p0-config.xml。配置文件中配置的各项参数,如表14.12所示。

表14.12 C3P0连接池配置参数

连接池配置参数

描述

maxPoolSize

连接池中拥有的最大连接数,如果获得新连接时会使连接总数超过这个值则不会再获取新连接,而是等待其他连接释放

minPoolSize

连接池保持的最小连接数

initialPoolSize

连接池初始化时创建的连接数

maxIdleTime

连接的最大空闲时间,如果超过这个时间,某个数据库连接还没有被使用,则会断开掉这个连接

driverClassName

JDBC数据库驱动类的类名

jdbcUrl

连接路径

user

数据库用户名

password

数据库密码

接下来,通过案例来演示C3P0连接池的使用,如例14-17所示。

14-17 以前面小节创建的数据库school为例,使用C3P0连接池实现对数据库的访问。在该例中需要创建C3P0连接池配置文件c3p0-config.xml和代码文件Demo1417.java。

步骤1: 在项目的src目录下创建C3P0连接池配置文件c3p0-config.xml,代码如下:

1 <?xml version="1.0" encoding="UTF-8"?>

2 <c3p0-config>

3 <default-config>

4 <!—-数据库驱动类-->

5 <property name="driverClass">com.mysql.jdbc.Driver</property>

6 <!—-数据库连接地址-->

7 <property name="jdbcUrl">jdbc:mysql://localhost:3306/school?useUnicode=

8 true&characterEncoding=utf-8&useSSL=false</property>

9 <!—-数据库用户名-->

10 <property name="user">root</property>

11 <!—-数据库密码-->

12 <property name="password">root</property>

13 <!—-初始连接数-->

14 <property name="initialPoolSize">10</property>

15 <!—-连接最大空闲时间-->

16 <property name="maxIdleTime">30</property>

17 <!—-连接池最大连接数-->

18 <property name="maxPoolSize">100</property>

19 <!—-连接池最小连接数-->

20 <property name="minPoolSize">10</property>

21 </default-config>

22 </c3p0-config>

步骤2:在项目src目录下下新建包com.aaa.p140502,并在该包下创建代码文件Demo1417.java,实现通过C3P0连接池访问数据库,代码如下:

1 package com.aaa.p140502;

2 import java.sql.Connection;

3 import java.sql.PreparedStatement;

4 import java.sql.ResultSet;

5 import java.sql.SQLException;

6 import java.util.Properties;

7 import com.mchange.v2.c3p0.ComboPooledDataSource;

8

9 public class Demo1417 {

10     public static void main(String[] args) throws SQLException {

11         // 创建C3P0连接池

12         ComboPooledDataSource dataSource = new ComboPooledDataSource();

13         // 从连接池中获取数据库连接

14         Connection con = dataSource.getConnection();

15         // 定义插入的sql

16         String sql = "insert into students " + " (name, gender, age)"

17                  + " values" + " ('张三','',20) ";

18         // 创建预编译命令执行对象, 绑定插入sql语句

19         PreparedStatement pst = con.prepareStatement(sql);

20         // 执行插入操作

21         int count = pst.executeUpdate();

22         // 输出插入的记录数

23         System.out.println("插入的记录数是: " + count);

24         // 定义查询的sql语句

25         sql = "select id, name, gender, age from students ";

26         // 创建预编译命令执行对象,绑定查询sql语句

27         pst = con.prepareStatement(sql);

28         // 执行查询并返回结果集对象

29         ResultSet rs = pst.executeQuery();

30         System.out.println("查询的结果是:");

31         // 通过结果集对象循环遍历查询的表数据

32         while(rs.next()) {

33             System.out.println(rs.getInt(1) + " " + rs.getString(2) + " " +

34                      rs.getString(3) + " " + rs.getInt(4));

35         }

36         // 关闭数据库对象

37         if(rs != null) {

38             rs.close();

39         }

40         if(pst != null) {

41             pst.close();

42         }

43         if(con != null) {

44             con.close(); // 将连接放回连接池

45         }

46     }

47 }

程序运行结果如下:

插入的记录数是: 1

查询的结果是:

1 张三 20

例14-17中,首先需要在程序中引入C3P0连接池需要的jar文件。然后,在src根目录下创建一个xml配置文件c3p0-config.xml,在该文件中配置了C3P0连接池需要使用的相关参数。接着,创建了一个测试类,在测试类的main方法中,直接使用ComboPooledDataSource创建了C3P0连接池的数据源对象dataSource。然后,使用dataSource从连接池中获取一个Connection连接对象。接着,通过连接对象创建了PreparedStatement对象,并绑定插入的SQL语句。接着,使用PreparedStatement对象执行插入操作,向数据库插入记录,并将插入的记录数打印输出。之后,再次通过连接对象创建一个新的PreparedStatement对象,并绑定查询的SQL语句。接着,通过PreparedStatement对象执行查询操作,并返回一个ResultSet结果集对象。然后,使用结果集对象通过循环的方式将查询到的数据逐条输出。最后,依次关闭数据库对象,需要注意的是,与DBCP连接池的用法一样,在调用连接对象的close()方法时,连接并没有被销毁而是被放回到连接池中了,下次需要使用时可以直接使用。

14.6 本章小结

• JDBC(Java Database Connectivity)是Java数据库访问技术。

• JDBC常用的API:Driver,DriverManager,Connection, Statement, PreparedStatement, ResultSet。

• DriverManager提供的主要操作就是得到一个数据库的连接,其中getConnection()方法就是取得连接对象。

• 所有的数据库的操作都是从Connection接口开始的。Connection指的是与特定数据库的连接。

• Statement 是 Java 执行数据库操作的一个重要接口,用于在已经建立数据库连接的基础上,向数据库发送要执行的SQL语句。

• PreparedStatement是Statement的子接口,可以传入带占位符的SQL语句。

• CallableStatement是Statement接口的子接口,可以接收过程的返回值,主要用于调用数据库中的存储过程

• ResultSet是数据中查询结果返回的一种对象是一个存储查询结果的对象,在JDBC操作中数据库的所有记录需要使用ResultSet进行接收,并使用ResultSet显示内容。

• 事务是保证数据库中数据完整性与一致性的重要机制。事务是由一组SQL语句组成的,这组语句要么全部执行成功要么全部执行失败。

• 事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)4个特性,这4个特性也被程为ACID特征。

• 在JDBC中处理事务,都是通过Connection对象完成的。同一事务中所有的操作,都必须使用同一个Connection对象。

• DAO 模式提供了访问关系型数据库系统所需操作的接口,将数据访问和业务层进行分离,并对业务层的调用提供了对应的数据访问接口。

• 数据库连接池技术负责分配、管理和释放数据库连接,它允许程序重复使用一个现有的数据库连接。

• DBCP 数据库连接池是 Apache 软件基金组织下的开源连接池组件。

• C3P0是一个开源的数据库连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。

14.7 理论试题与实践练习

1.选择题

1.1 使用JDBC事务的步骤是( )

A.取消Connection的事务自动提交方式 B.发生异常,回滚事务

C.获取Connection对象                  D.数据操作完毕提交事务

1.2 下列选项那个不是JDBC事务的特征( )

A.原子性 B.隔离性 C.并发性 D.持久性

1.3 下列选项是DAO模式的优点有( )

A.隔离了业务逻辑代码和数据访问代码 B.分工明确

C.降低耦合性,提高可重用性 D.造成类的泛滥,降低了可维护性

1.4 一个经典到DAO模式包括如下几个部分( )

A.一个数据库访问工具类,主要负责通用增删改查的实现

B.DAO接口,定义数据库访问方法

C.DAO实现类,实现DAO接口,完成具体功能

D.实体类,封装和传递数据

2.实践练习

2.1 编写数据库工具类BaseDAO,实现通用的增删改查功能。

训练技能点:

• DAO模式。

• 获取数据库连接。

• 关闭相关资源。

• 数据库的增删改查操作。

需求说明:

项目中经常要使用数据库连接,进行添加、删除、修改、查询等操作,编写一个BaseDAO实现相关方法,实现代码的复用。

实现步骤:

• 编写获取连接的方法。

• 编写执行增删改的方法。

• 编写通用查询的方法。

• 编写参数设置的方法。

• 编写关闭相关资源的方法。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。