JDBC入门

学习之前先要弄明白的是:什么是 JDBC?

SUN 公司为了简化、统一对数据库的操作,定义了一套 Java 操作数据库的规范,称之为 JDBC

我们要操作数据库,需要有数据库厂商提供的驱动,但是它们的规范很可能是不同的,难道我们要把所有的数据库规范都看一遍?学习成本太高,不利于推广啊,于是 sun 公司就搞了个 JDBC (接口),所有的驱动按照它的规范来,这样我们用的时候只要导入相应的驱动,代码统一使用 JDBC 的规范就可以了,降低了学习成本

JDBC.jpg

其实 Servlet 也是一样的思想,也是接口,是一种规范;比如我使用的是 Tomcat ,那么具体实现就是 Tomcat 来完成的,这就是我们为什么要导入 Tomcat 中的相关 jar

我的机器只有 MySQL 的,所以我就只是用的它做的测试,驱动下载的官方地址:
https://www.mysql.com/products/connector/
使用 IDEA 的同学可以直接在 DATABASE的侧边栏 设置里自动下载哦

基本套路

使用 JDBC 的基本套路可以用下面的代码来表示,一共六点
需要注意的是:java.sql.*javax.sql.* 都属于 JavaSE 了,所以建个普通工程就可以用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Main {
// URL 后面建议加 ?useUnicode=true&characterEncoding=UTF-8 属性来确保编码
// 如果是连本机可以省略:jdbc:mysql:///test
private static String url = "jdbc:mysql://115.159.200.212:3306/test";
private static String userName = "Loli";
private static String password = "xxxxx";

public static void main(String[] args) throws SQLException {
// 1. 加载驱动,推荐第二种
// DriverManager.registerDriver(new Driver());
Class.forName("com.mysql.jdbc.Driver");

// 2. 获取链接
Connection conn = DriverManager.getConnection(url, userName, password);

// 3. 获取向数据库发sql语句的 statament 对象
Statement statement = conn.createStatement();

// 4. 向数据库发送 SQL,获取结果集
ResultSet resultSet = statement.executeQuery("SELECT * FROM users;");

// 5. 从结果集获取数据
while (resultSet.next()){
System.out.println(resultSet.getObject("id"));
System.out.println(resultSet.getObject("name"));
System.out.println(resultSet.getObject("sex"));
}

// 6. 释放链接,顺序是反着的
resultSet.close();
statement.close();
conn.close();
}
}

在第一步的加载驱动的时候,有两种方法:
第一种不推荐,因为首先它会被加载两次(可以看源代码中的静态代码块),其次会有强依赖性,必须要 import 相关 jar 包;
而第二种只需要一个字符串而已,在更换数据库的时候更方便。

不得不再感慨,IDEA 的代码提示救了我这英语白痴+健忘,连 SQL 语句和配置文件的内容都可以提示…..

防止在读取/存储时中文出现乱码,记得设置 url 的时候,结尾加上 ?useUnicode=true&characterEncoding=UTF-8 ,如果报错了,反正就是提示 url 有问题,那么可以尝试转义一下试试,转义后的 url 为:?useUnicode=true&characterEncoding=UTF-8
学过 html 的都懂哈….

Statement对象

从上面的代码也可以看出,这个是用来向数据库发送 SQL 语句的;常见的几个方法有:

  • executeQuery()
    执行查询语句,返回一个结果集
  • executeUpdate()
    执行增删改语句,返回影响的行数;判断是否大于 0 来判断是否执行成功
  • execute()
    执行所有的 SQL 语句,但是一般不太用,因为它返回的是一个 boolean ,代表是否执行成功
    比如查询,我们还需要判断、再用 getResultSet 来获取结果集,太麻烦

preparedStatement对象

在实际中,其实使用 preparedStatement 的情况比较多,从名字上就可以猜出它与 Statement 一定关系不一般
statement 和 preparedStatement 的区别:

  1. preparedStatement 是 statement 的孩子,也就是 preparedStatement 继承自 statement
  2. preparedStatement 可以防止 SQL 注入问题,设置的参数会被转义
  3. preparedStatement 可以对 SQL 语句进行预编译,提高效率
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void test() {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;

try {
conn = JdbcUtils.getConnection();
String sql = "SELECT * FROM users WHERE id = ?";
st = conn.prepareStatement(sql); // 预编译 sql
st.setInt(1, 1); // 补全设置的占位符

rs = st.executeQuery();

//TODO
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtils.release(conn, st, rs);
}
}

JdbcUtils 的写法可以参考:Github

ResultSet对象

查询返回的结果集,可以简单理解为一个表格,有一个游标,和 Android 一样默认是指向第一行之前 ;所以一般是先进行 next 获取
它提供了一堆的 get 方法,获取各种类型的数据,当然无论那种数据都可以使用万能的 getObject
所有的 get 方法都提供了两种重载,可以使用索引号或者列名获取一行的数据注意索引从 1 开始 ,为了更直观,一般不会使用索引来获取;如果结果集只有一列,那么用索引就比较方便了

如果结果集只有一行数据,可以使用 if(resultSet.next()){} ,有多行就要用 while(resultSet.next()){} 来循环取出

其他的常用方法:

  • 下一行:rs.next();
  • 上一行:rs.previous();
  • 移动到指定行:
    可以是负数!如果传入1,相当于 first(); ;如果传入 -1 ,相当于 last();
    滚动后是可以直接取数据
    rs.absolute(indexNum);
  • 移动到最前,第一行之前
    rs.beforeFirst();
  • 移动到最后,最后一行之后
    rs.afterLast();

批量操作

如果想要同时执行多条 SQL 语句,那么一条一条的去执行看起来比较弱鸡,资源开销太大,效率太低,所以肯定有批量的法子啊!就是这么自信
既然执行 SQL 语句有两种方式,那么类似的批量操作应该也有两种方式 : 使用 statement 和使用 preparedStatement

使用statement

非常非常简单,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 可以同时执行不同类别的语句,但是效率低
private static void statementTest() {
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
st = conn.createStatement();

st.addBatch("insert into temp(id,value) VALUES (100,'lolicon')");
st.addBatch("UPDATE temp SET id=101 WHERE id=1");
st.executeBatch();
st.clearBatch();
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtils.release(conn, st, rs);
}
}

就是通过 addBatch 方法添加进 List 集合,然后 executeBatch 一起执行,再用 clearBatch 清空 List
就当作 createStatement 方法返回的 st 内部维护了一个 List 集合嘛~我也没看源码实现,应该是差不多的 (:雾

使用preparedStatement

其实它们是比较类似的,事实上这一种用的比较多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 只能执行相同的语句,用于批量插入、更新等;效率高
private static void preparedStatementTest() {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
st = conn.prepareStatement("insert into temp (id, value) values (?,?);");
// 插入 30 条数据
for (int i = 0; i < 30; i++) {
st.setInt(1, i);
st.setString(2, "test" + i);
// 添加到 List 集合
st.addBatch();
// 每满 10 条发送一次
if (i % 10 == 0) {
st.executeBatch();
st.clearBatch();
}
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtils.release(conn, st, rs);
}
}

和上面一种基本一致,只能预编译一条 SQL ,所以……

因为 List 存在内存中,JVM 的内存也是有限的,所以如果要执行很多条 SQL 的话建议分段
就向上面一样,每满多少条就执行一下,然后清空 List
当然我这里设的 10 太小了,只为测试,设个 1000 应该也不多,具体多大凭感觉…

调用存储过程

至于什么是存储过程我在 MySQL 的文章中说过了,简单说就是一个函数;所以也就是说如何调用函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
Connection conn = null;
CallableStatement st = null; // 注意
ResultSet rs = null;

try {
conn = JdbcUtils.getConnection();
st = conn.prepareCall("{call demo(?,?)}");
st.setString(1,"lolicon");
// 设置输出的类型
st.registerOutParameter(2,Types.VARCHAR);
st.execute();

// 获取存储过程的输出值
String str = st.getString(2);
System.out.println(str);

} catch (SQLException e) {
e.printStackTrace();
}finally {
JdbcUtils.release(conn,st,rs);
}
}

关键在于使用 prepareCall 来调用函数,在 MySQL 中,函数的返回值也写在参数,这是不太一样的地方,总之还是比较简单的

获取自动生成的键

在插入数据的时候,如果某一列我们设置为自动增长,通常也设置为主键,这样在插入的时候就不需要管这列了,但是如果后续操作需要用到这一列,那就得再查询一次,这样肯定不爽,所以要自信的认为有个插入数据的时候会返回这列的值的方法;也就是说这东西可以返回它自动生成的值
当然, 仅对于 insert 有效,因为只有插入的时候才会生成 ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static void getID() {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
// 第二个参数是是否返回主键,mysql 是默认返回可以不写,写上最好
st = conn.prepareStatement("insert into keytemp (val) VALUES ('abcd')", Statement.RETURN_GENERATED_KEYS);
st.executeUpdate();
// 获取返回的主键,如果没有就返回空的 ResultSet 对象
rs = st.getGeneratedKeys();
while (rs.next()) {
System.out.println(rs.getInt(1));
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
JdbcUtils.release(conn,st,rs);
}
}

官方 API 上说:

获取由于执行此 Statement 对象而创建的所有自动生成的键。如果此 Statement 对象没有生成任何键,则返回空的 ResultSet 对象。
注:如果未指定表示自动生成键的列,则 JDBC 驱动程序实现将确定最能表示自动生成键的列。

关于表设计

一般来说一个对象(javabean)对应一个表,大部分也就是下面的几种情况

当两个表有关联的时候,比如一对多,对应到 bean 中就是主 bean (相当于主表)中有个 set 集合保存子 bean ,子 bean 包含一个主 bean 类型

一对多/多对一

  1. 先不要管映射关系,先设计出基本的属性
  2. 在多的一方加外键描述

但是呢,尽量不要使用一对多,能不用就不用,因为有时候做查询,返回有太多的数据会导致内存溢出,具体可以考虑显示的需求,如果“一”中不需要显示“多”,那么就不需要查询多的一方了

多对多

  1. 先不要管映射关系,先设计出基本的属性
  2. 设计一个中间表,一般两列;命名为:两个表名中间用 _ 连接
  3. 中间表要加约束,对应各自的 id , 当然也可以使用联合主键保证不重复

一对一(主从关系)

  1. 先不要管映射关系,先设计出基本的属性
  2. 从表要加外键约束、非空、不能重复、来自主表

某些情况可以把主键设置成外键所在列

自连接

  1. 先设计基本属性
  2. 设计一列,增加外键约束,约束来自本表,不要加非空

名字参考:parent_id
主要是用来做无限分级(比如分类表),但是如果层次太深查询使用递归的时候就很容易导致内存溢出,有一种解决方案是使用树结构(树状节点),其实就是二叉树的先序遍历,这个以后再说吧

其他注意

Connection 连接非常的宝贵,因为数据库支持的连接数是非常有限的,所以要遵循 尽量晚创建,尽量早释放 的原则;提示,释放连接的代码要写在 finally 里,来确保一定会释放,一般的套路是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void main(String[] args) throws SQLException, ClassNotFoundException {
Connection conn = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 1. 加载驱动
// 2. 获取链接
// 3. 获取向数据库发sql语句的 statament 对象
// 4. 向数据库发送 SQL,获取结果集
// 5. 从结果集获取数据
} finally {
// 6. 释放链接,顺序是反着的;保证绝对释放
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}

关于 JDBC 中 Java 和 MySQL 对应的数据类型到这里查看:
http://wiki.jikexueyuan.com/project/jdbc/data-types.html

JDBC 的使用其实有很多重复代码,最好使用抽出来形成一个工具类,比如获取 conn 和 释放连接的方法,就像上面给的地址中的那样

更多的内容,比如事务等下次再写吧…

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~