上一章讲解了JSP的高级标签运用,链接:EL表达式 && JSTL标签库
任何软件,除去文档和程序以外,剩下的就是数据了。那么在学习JavaWeb的途中就得学会对数据库的访问,就得提及JDBC。
目录
何为JDBC?
JDBC的组成
JDBC的使用
添加、改变、删除记录
查询记录
JDBC的拓展知识
结果集ResultSet
光标
元数据
特性
预编译PreparedStatement
批处理
时间类型转换问题
存储数据大的文件
事务
定义
通俗理解
特性(ACID)
MySQL里的事务
JDBC事务
事务隔离级别
其他数据库连接的URL格式
源码剖析
后话
JDBC(Java Database Connectivity)就是Java数据库连接,说白了就是使用Java语言向数据库发送SQL语句来操作数据库。
提及一下以前的历史,SUN公司与各大数据库厂商讨论,最终得出一个结论:由SUN公司提供一套访问数据库的规范(一组接口),并提供连接数据库的协议标准,然后各大数据库厂商会遵循SUN的规范提供一套访问自己公司的数据库服务器的API。而SUN公司提供的规范命名为JDBC,而各个厂商提供的遵循了JDBC规范的访问所属数据库的API被称之为驱动。
那么就可以得出JDBC地组成:JDBC Driver Interface 以及 JDBC API。
JDBC Driver Interface:是面向JDBC驱动程序开发商的编程接口,它会把我们通过JDBC API发给数据库的通用指令翻译给他们自己的数据库。
JDBC API:是提供给开发者的一组独立于数据库的API,对任何数据库的操作,都可以用这组API来进行。那么要把这些通用的API翻译成特定数据库能懂的"指令",就要由JDBC Driver Interface来实现了。
JDBC有四大配置参数:驱动类名称、连接的数据库url、用户名以及密码。对于数据库url来说,不同的数据库厂商有不同的数据库格式。接下来使用其对数据库进行增删查改,以MYSQL数据库为例。
首先是连接数据库模块:
String driverClassName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/student";
String userName = "root";
String password = "123456";
// 1.加载驱动
Class.forName(driverClassName);
// 2.获得Connection
Connection con = DriverManager.getConnection(url, userName, password);
值得一提的是,其中加载驱动类是可以省略的,但为了兼容低版本的JDBC,还是写上为妙。因为在JDBC4.0后,每个驱动的Jar包中,在META-INF/services目录下提供了一个名为java.sql.Driver的文件,文件的内容就是该接口的实现类名称,即如下所示:
回到正题,这几串代码又是如何产生联系的呢?找到com.mysql.jdbc.Driver的源文件,其中有一个static块,块内的代码如下所示:
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
从块内的代码来看,块内的内容就是把自己(驱动)注册到DriverManager中。那么当执行Class.forName方法时就会初始化驱动类,并且自行执行static块内的代码,以达到注册驱动的目的。
接下来对数据库进行增删改查,其顺序是通过Connection得到Statement对象,再由Statement对象发送sql语句。
先创建一个message表,相关字段设置如下所示:
CREATE TABLE message (
number char(15) PRIMARY KEY,
name char(15),
age int(5)
);
Connection con = null;
try {
// 连接数据库的四大参数
String driverClassName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/student";
String userName = "root";
String password = "123456";
// 1.加载驱动
Class.forName(driverClassName);
// 2.获得Connection
con = DriverManager.getConnection(url, userName, password);
// 3.通过Connection得到Statement对象
Statement stmt = con.createStatement();
// 4.使用Statement发送sql语句
// String sql = "INSERT INTO message VALUES('0001', 'zhangsan', 18)";
// String sql = "UPDATE message SET age=30 where number='0001'";
String sql = "DELETE FROM message";
// 返回影响的行数
int total = stmt.executeUpdate(sql);
System.out.println(total);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e.getMessage());
} finally {
if (con != null)
con.close();
}
结果如图所示:
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try {
String driverClassName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/student";
String userName = "root";
String password = "123456";
// 1.加载驱动
Class.forName(driverClassName);
// 2.获得Connection
con = DriverManager.getConnection(url, userName, password);
// 3.通过Connection得到Statement对象
stmt = con.createStatement();
// 4.使用Statement发送sql语句
String sql = "SELECT * FROM message";
// 返回结果集
rs = stmt.executeQuery(sql);
while (rs.next()) {
// 可以用数字代表表中的第几列获取记录
String id = rs.getString(1);
// 也可以直接用表中的列名获取记录
// rs.getString("number");
String name = rs.getString(2);
int age = rs.getInt(3);
System.out.println(id + "---" + name + "---" + age);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e.getMessage());
} finally {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (con != null) con.close();
}
结果如图所示:
Statement接口中主要有三个方法来执行单条SQL语句:
// 以下三个方法都是在java.sql.Statement中
public abstract ResultSet executeQuery(String s) throws SQLException;
public abstract int executeUpdate(String s) throws SQLException;
public abstract boolean execute(String s) throws SQLException;
这三种方法异同:
executeQuery方法执行查询操作,结果返回一个结果集ResultSet
executeUpdate方法执行更新操作,即执行insert、update、delete语句。实际上这个方法也可以执行DDL语句,即create table等语句。
execute方法可以执行增删查改所有SQL语句,值得一提的是,该方法返回的值是boolean类型,表示SQL语句是否执行成功。但是这个方法如果用于执行更新操作,则还需要再调用getUpdateCount()方法来获取更新操作影响的行数。如果用于执行查询操作,那么还要调用getResultSet()方法来获取查询结果。
那么在这里总结一下使用JDBC连接数据库并且执行相关语句的步骤:
1. 配置好连接数据库的四大参数(驱动类名称、连接的url、用户名、密码);
2. 通过驱动类名称来加载相应的驱动;
3. 通过DriverManager以及后三个参数来初始化连接Connection;
4. 根据Connection获取Statement对象后执行所需的SQL语句
ResultSet中有一个行光标(游标),可以用其来滚动结果集。光标位置有四个可选值:BeforeFirst、First、Last、AfterLast。其中,BeforeFirst和AfterLast称为不存在记录的位置。而First、Last分别代表了整个结果集中记录的第一行和最后一行。
ResultSet提供了一系列移动游标以及判断游标位置的方法,如下所示:
// 判断光标位置
public abstract boolean isBeforeFirst() throws SQLException; // 当前光标位置是否在第一行前面
public abstract boolean isAfterLast() throws SQLException; // 当前光标位置是否在最后一行的后面
public abstract boolean isFirst() throws SQLException; // 当前光标是否在第一行上
public abstract boolean isLast() throws SQLException; // 当前光标是否在最后一行上
// 移动光标
public abstract void beforeFirst() throws SQLException; // 将光标放至第一行的前面
public abstract void afterLast() throws SQLException; // 将光标放至最后一行的后面
public abstract boolean first() throws SQLException; // 将光标放至第一行的位置
public abstract boolean last() throws SQLException; // 将光标放到最后一行的位置
public abstract boolean next() throws SQLException; // 将光标向下挪一行
public abstract boolean absolute(int i) throws SQLException; // 绝对位移,将光标移动到指定行上
public abstract boolean relative(int i) throws SQLException; // 相对位移,参数为正向上移动n行,反之向下移动n行
public abstract boolean previous() throws SQLException; // 将光标向上挪一行
需要注意的是,光标的默认位置是beforeFirst。并且还有一点需要注意的是,结果集如果是不可滚动的只能使用next()方法。而结果集是否支持滚动,则取决于创建的Statement。在Connection中除去无参的createStatement方法外还有一个带参的同名方法,其如下所示:
// 在源代码中其实参数为i,j,这里为了方便记忆,更换了一下名字
public abstract Statement createStatement(int resultSetType, int resultSetConcurrency)
resultSetType可选值有(使用时必须指定ResultSet前缀):
TYPE_FORWARD_ONLY:不滚动结果集;
TYPE_SCROLL_INSENSITIVE:滚动结果集,但结果集数据不会再跟随数据库变化而变化;
TYPE_SCROLL_SENSITIVE:滚动结果集,但结果集数据会跟随数据库变化而变化。(无驱动支持)
resultSetConcurrency可选值有:
CONCUR_READ_ONLY:结果集只读,不能通过修改结果集影响数据库;
CONCUR_UPDATABLE:结果集可更新,对结果集的更新可反向影响数据库。
使用例子如下:
public static Connection con = null;
public static ResultSet rs = null;
public static Statement stmt = null;
public static PreparedStatement pstmt = null;
static {
String className = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/student?rewriteBatchedStatement=true";
String userName = "root";
String password = "123456";
try {
Class.forName(className);
con = DriverManager.getConnection(url, userName, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Test
public void fun1() throws SQLException {
stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
String sql = "select * from message";
rs = stmt.executeQuery(sql);
// 移动游标至最后一行的后面
rs.afterLast();
while (rs.previous()) {
System.out.println(rs.getString(1) + "---" + rs.getString(2) + "---" +
rs.getInt(3));
}
}
结果如下所示:
元数据表示的是结果集的相关信息,其中包括了列名称,列数量等。在ResultSet接口中有一个方法来获取元数据,其存取在ResultSetMetaData里。
public abstract ResultSetMetaData getMetaData() throws SQLException;
ResultSetMetaData中可以获取结果集列数、指定列的列名,如下所示:
public abstract int getColumnCount() throws SQLException;
public abstract String getColumnName(int i) throws SQLException;
是否可滚动:即游标是否可移动(建议不滚动,性能问题)
是否敏感:即对修改敏不敏感(A正查看数据,B修改数据,敏感则A看到修改后的数据,不敏感则A看的不是最新数据)
是否可更新:即更新结果集是否影响数据库
在提及预编译之前,得先稍微提及一下数据库对SQL语句的执行过程:
词法分析=》语法分析=》语义分析=》执行计划优化=》执行
词法分析和语法分析称之为硬解析,每一次执行一条语句都要经历上述过程,若是相同语句,则都要经历硬解析,大大降低了性能。因此,预编译就诞生了。
预编译是将SQL语句中的值用占位符?来替代,意思就是将SQL语句模板化,这样就可以达到“一次编译,多次运行”的效果。
还有一个比较不是那么实用性的作用就是防SQL注入(其实前端即可完成校验)
那么SQL注入又是什么呢?接下来演示一下SQL注入,代码如下所示:
@Test
public void fun2() throws SQLException {
String name = " a' or 'a'='a ";
stmt = con.createStatement();
String sql = "select * from message where name = '" + name + "'";
System.out.println(sql);
rs = stmt.executeQuery(sql);
System.out.println(rs.next());
}
结果如下所示:
为什么会输入了非数据库内存有的数据依旧返回true呢?这是因为单引号在语句中将语句截断然后拼成了一个更加完整的语句。如下所示:
后面的'a' = 'a'为真,即或运算返回真,相当于where字句后条件为真,等同于select * from message,即返回所有表内的记录。
而预编译用占位符来替代值就避免了单引号的问题,那么现在来体验一下预处理:
Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
String driverClassName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/student";
String userName = "root";
String password = "123456";
// 1.加载驱动
Class.forName(driverClassName);
// 2.获得Connection
con = DriverManager.getConnection(url, userName, password);
String sql = "SELECT * FROM message where name = ?";
stmt = con.prepareStatement(sql);
stmt.setString(1, "zhangsan");
// 返回结果集
rs = stmt.executeQuery();
while (rs.next()) {
// 可以用数字代表表中的第几列获取记录
String id = rs.getString(1);
// 也可以直接用表中的列名获取记录
// rs.getString("number");
String name = rs.getString(2);
int age = rs.getInt(3);
System.out.println(id + "---" + name + "---" + age);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e.getMessage());
} finally {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (con != null) con.close();
}
结果如下所示:
至于其是如何处理的,将在下面源码剖析时讲解。
这里还得提及一个参数cachePrepStmts。当使用不同的preparedStatement对象来执行相同语句时,依旧会出现编译两次的现象,原因在于驱动并没有保存编译后的函数key。要解决这个问题则需要在url后设置cachePrepStmts为true。
默认情况下Mysql的批处理是关闭的,需要设置参数打开,即在url后设置rewriteBatchedStatement=true。
批处理则是一次向服务器发送多条SQL语句,然后服务器将一次性处理。需要注意的是,批处理只针对更新语句。
那么在statement中就有专门处理批处理的方法,如下所示:
public abstract void addBatch(String s) throws SQLException;
public abstract void clearBatch() throws SQLException;
public abstract int[] executeBatch() throws SQLException;
接下来演示一下批处理:
public static Connection con = null;
public static ResultSet rs = null;
public static Statement stmt = null;
public static PreparedStatement pstmt = null;
static {
String className = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/student?rewriteBatchedStatement=true";
String userName = "root";
String password = "123456";
try {
Class.forName(className);
con = DriverManager.getConnection(url, userName, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Test
public void fun3() throws SQLException {
String sql = "INSERT INTO message values(?, ?, ?)";
pstmt = con.prepareStatement(sql);
for(int i = 0; i < 10000; i++) {
pstmt.setString(1, String.valueOf(i));
pstmt.setString(2, "demo_" + i);
pstmt.setInt(3, i);
// 添加批,这一组参数保存到一个集合里
pstmt.addBatch();
}
// 执行批
long a = System.currentTimeMillis();
pstmt.executeBatch();
long b = System.currentTimeMillis();
System.out.println(b - a);
}
结果如下所示:
在初期学习的时候,当碰到时间类型时,会发现类型不一致问题。在ResultSet的setDate和getDate方法中,其Date类型是属于java.sql包下并非常用的java.util包下的Date类型。那么查看了一下相关的源码发现,java.sql.Date是java.util.Date的子类。
那么,当获取数据库中表里的时间类型赋值到对象的时间属性时,可以直接赋值给对象的属性。
而当想要将时间添加到表中时,就得获取时间的毫秒值,再用毫秒值创建sql的Date。
以下为代码演示:
// java.sql.Date、Time、Timestamp ==> java.util.Date
java.util.Date date = java.sql.Date;
// java.util.Date ==> java.sql.Date、Time、Timestamp
java.util.Date date = new Date();
long t = date.getTime();
java.sql.Date sqlDate = new Date(t);
在Mysql中只提供了以下四种类型来处理较大的文件:
类型 | 长度 |
tinytext | |
text | |
mediumtext | |
longtext |
以存储一个MP3文件为例,首先创建一个表:
CREATE TABLE music (
id INT PRIMARY KEY AUTO_INCREMENT,
fileName VARCHAR(100),
data MEDIUMBLOB
)
接着用相关的方法将MP3文件插入到数据库当中:
public void fun4() throws SQLException, FileNotFoundException, IOException {
String sql = "insert into music values(?, ?, ?)";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, 1);
pstmt.setString(2, "demo.mp3");
byte[] bytes = IOUtils.toByteArray(new FileInputStream("F:/demo.mp3"));
Blob blob = new SerialBlob(bytes);
pstmt.setBlob(3, blob);
pstmt.execute();
}
从数据库中读取MP3文件并且将其保存至硬盘:
public void fun5() throws SQLException, IOException {
String sql = "select * from music";
stmt = con.createStatement();
rs = stmt.executeQuery(sql);
if(rs.next()) {
Blob blob = rs.getBlob(3);
InputStream in = blob.getBinaryStream();
OutputStream out = new FileOutputStream("D:/demo.mp3");
IOUtils.copy(in, out);
}
}
需要注意的是,若出现了Parameter of prepared statement which is set through mysql_send_long_data()错误,则是在Mysql的根目录下的配置文件my-default.ini内配置如下设置:
max_allowed_packet=10485760
事务(全称:数据库事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
以银行转账为例,甲向乙转账100元,乙收到甲的转账100元,这两者的行为对数据库的操作语句即为一个事务,即事务是一个整体,是一堆操作语句的集合。
原子性(Atomicity):事务中所有操作都是不可再分割的原子单位。事务中所有操作要么全部执行成功,要么全部执行失败。
一致性(Consistency):事务执行后,数据库状态与其他业务规则保持一致。(转账操作中,两个账号的余额之和保持不变)
隔离性(Isolation):隔离性是指在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。
持久性(Durability):一旦事务提交成功,事务中所有的数据操作都必须持久化到数据库中,即时提交事务后,数据库崩溃,在数据库重启时,也必须能保证通过某种机制恢复数据。
值得一提的是:其他特性都是为一致性而服务的。
默认情况下MySQL对每一条语句都认为是一个单独的事务。要想在一个事务里包含多条SQL语句,则需要开启事务和结束事务。
开启事务:START TRANSACTION | BEGIN
关闭事务:COMMIT | ROLLBACK
开启事务后会执行多条SQL语句,最后要结束事务,COMMIT表示提交,即事务中所有SQL语句对数据库造成的影响将会持久化到数据库内。而ROLLBACK表示回滚,即回滚到事务最开始阶段,之前所有的SQL语句操作都被撤销。
这里演示一下转账操作(张三账户原先900,李四账户原先1100):
当输入的是ROLLBACK时,显示结果如下:
演示完MySQL中的事务后,来讲一下JDBC中是如何处理事务。
首先得明确一点,在JDBC中处理事务都是通过Connection完成的。
在JDBC规范中,Connection接口里有以下方法来处理事务:
// 设置是否自动提交事务,默认为true
public abstract void setAutoCommit(boolean flag) throws SQLException;
// 获取自动提交事务功能是否开启
public abstract boolean getAutoCommit() throws SQLException;
// 提交结束事务
public abstract void commit() throws SQLException;
// 回滚结束事务
public abstract void rollback() throws SQLException;
需要注意的地方:设置是否自动提交事务的setAutoCommit()方法参数默认值为true,即代表每条执行的SQL语句都是一个单独的事务。设置为false则相当于开启事务。
在说起事务隔离级别之前,先说起并发事务问题。并发事务导致的问题大致分别五类,其中两类是更新问题,三类是读问题。在这里先讲读问题。
脏读:读到另一个事务未提交更新的数据,即读取到脏数据。
不可重复读:对同一记录的两次读取不一致,因为另一事务对该记录做了修改。
虚读:对同一张表的两次查询不一致,因为另一事物插入了一条记录。
这里只举例脏读案例:
'''
事务1:张三给李四转账100元
事务2:李四查看账户余额
事务1:开始事务;
事务1:张三给李四转账100元;
事务2:开始事务;
事务2:李四查看账户余额,账户多出100元(脏读);
事务2:提交事务;
事务1:回滚事务,李四账户回到转账之前的状态
'''
现在再来说回四大隔离级别:
SERIALIZABLE(串行化):三种读问题皆可解决;性能最差。
REPEATABLE READ(可重复读)(MySQL):防脏读和不可重复读;性能比前者好。
READ COMMITTED(读已提交数据)(Oracle):防止脏读;性能比前者好。
READ UNCOMMITTED(读未提交数据):啥也不处理,即任何并发问题都会出现;性能最好。
在MySQL中通过以下命令查看默认隔离级别:
select @@tx_isolation
在JDBC中通过Connection中的一个方法来设置隔离级别:
public abstract void setTransactionIsolation(int i) throws SQLException;
/*
* 参数可选值如下:
* public static final int TRANSACTION_NONE = 0;
* public static final int TRANSACTION_READ_UNCOMMITTED = 1;
* public static final int TRANSACTION_READ_COMMITTED = 2;
* public static final int TRANSACTION_REPEATABLE_READ = 4;
* public static final int TRANSACTION_SERIALIZABLE = 8;
*/
#mssql
driverClassName = com.microsoft.jdbc.sqlserver.SQLServerDriver
url = jdbc:sqlserver://127.0.0.1:端口号;DatabaseName=数据库名
#mssql jtds
driverClassName = net.sourceforge.jtds.jdbc.Driver
url = jdbc:jtds:sqlserver://127.0.0.1:端口号;DatabaseName=数据库名
#orcale
driverClassName = oracle.jdbc.driver.OracleDriver
url = jdbc:oracle:thin:@localhost:端口号:数据库名
#access
driverClassName = sun.jdbc.odbc.JdbcOdbcDriver
url = jdbc:odbc:driver={Microsoft Access Driver (*.mdb)};DBQ=mdb\\数据库名.mdb
'''
JDBC有很多可以深究的地方,MySQL的部分差不多就写这么多,接下来就是连接池部分
'''