参考: (JDBC常见面试题集锦1)http://blog.youkuaiyun.com/spidercoco/article/details/21439057
(JDBC常见面试题集锦2)http://blog.youkuaiyun.com/spidercoco/article/details/21508763
JDBC简介
JDBC接口及相关类在java.sql包和javax.sql包里。我们可以用它来连接关系型数据库,执行SQL查询,存储过程,并处理返回的结构。
JDBC接口让Java程序和JDBC驱动实现了松耦合,使得切换不同的数据库变得更加简单。
JDBC API使用Java的反射机制来实现Java程序和JDBC驱动的松耦合。所有操作都是通过JDBC接口完成的,而驱动只有在通过Class.forName反射机制来加载的时候才会出现。
JDBC驱动程序类型
- Type1:驱动程序将JDBC转换成ODBC。使用ODBC驱动连接数据库,需要安装ODBC以便连接数据库,这种方式已经基本淘汰了。
- Type2:这种驱动把JDBC调用适配成数据库的本地接口调用。
- Type3:使用与具体数据库无关的协议将数据库请求发送给服务器中间件,然后该中间件再将数据库请求翻译成特定数据库协议。这种方式增加了额外的网络调用,导致性能变差,因此很少使用。
- Type4:这个驱动把JDBC请求直接转化成特定数据库使用的网络协议。这种方案最简单,也适合通过网络连接数据库。不过使用这种方式的话,需要根据不同的数据库使用特定的驱动程序。
JDBC常用操作
创建JDBC连接 - Connection
JDBC连接时和数据库服务器建立的一个会话,可以想象成时一个和数据库的Socket连接。
注册并加载驱动
使用Class.forName(),驱动类就会注册到DriverManager里面并加载到内存中:
用DriverManager获取连接对象
调用DriverManager.getConnection方法并传入数据库连接的URL,用户名、密码,就能获取到连接对象。
创建JDBC连接所需的参数
创建JDBC连接器需要提供四个基本数据:
- JDBC驱动:driverClassName
- 数据库连接字:url
- 数据库访问用户名:username
- 数据库访问密码:password
JDBC驱动值
- mysql:com.mysql.jdbc.Driver
- oracle:oracle.jdbc.driver.OracleDriver
- sql server:com.microsoft.jdbc.sqlserver.SQLServerDriver
数据库连接字
- mysql:jdbc:mysql://DBComputerNameOrIP:3306/DBName
- oracle:jdbc:oracle:thin:@DBComputerNameOrIP:1512:DBName
- sql server:jdbc:microsoft:sqlserver://DBComputerNameOrIP:1433;databaseName=DBName
获取数据库服务器的相关信息 - DatabaseMetaData
使用DatabaseMetaData可以获取到服务器的信息。当和数据库建立了连接之后,可以通过调用Connection的getMetaData方法获取数据库的元信息。DatabaseMetaData里面有很多方法,通过它们可以获取到数据库的产品名称,版本号,配置信息等。
执行数据库SQL查询语句 - Statement
Statement是JDBC中用来执行数据库SQL查询语句的接口。通过调用连接对象的createStatement方法可以生成一个Statement对象。通过调用它的execute,execeteQuery,executeUpdate方法来执行静态SQL查询。
默认情况下,一个Statement同时只能打开一个ResultSet。如果想操作多个ResultSet的话,需要创建多个Statement。Statement接口的所有execute方法开始执行时都会默认关闭当前打开的ResultSet。
execute
用来执行任意的SQL语句。如果查询结果时一个ResultSet,这个方法就返回true。如果结果不是ResultSet,比如insert或update查询,就会返回false。可以通过getResultSet方法来获取ResultSet,或者通过getUpdateCount方法来回去更新的记录条数。
Note:execute方法不能用于PreparedStatement或者CallableStatement。
executeQuery
用来执行select查询,并且返回ResultSet。即使查询不到记录返回的ResultSet也不会为null(never null)。通常使用executeQuery来执行查询语句,如果传进来的是insert或者update的话,它会抛出SQLException。
Note:execute方法不能用于PreparedStatement或者CallableStatement。
executeUpdate
用来执行inset,update或者delete语句(DML),或者什么也不返回的DDL语句。如果时DML语句的话,返回更新的条数;如果时DDL语句的话,返回0.
Note:execute方法不能用于PreparedStatement或者CallableStatement。
只有当不确定时什么语句的时候才应该使用execute方法,否则应该使用executeQuery或者executeUpdate方法。
getGeneratedKeys
有时候表会生成主键,这时候就可以用Statement的getGeneratedKeys方法来获取这个自动生成的主键的值。
setMaxRows
可以用来限制返回的数据集的行数(通过SQL语句也可以实现这个功能)。
setFetchSize
当数据库在执行一条查询语句时,查询到的数据是在数据库的缓存中维护的。ResultSet其实引用的是数据库中缓存的结果。
假设我们有一条查询返回了100行数据,我么把fetchsize设置成了10,那么数据库驱动每次只会取10条数据,也就是说得取10次。当每条数据需要处理的时间较长的时候并且返回数据又非常多的时候,这个可选参数就变的非常有用了。
执行预编译的SQL语句 - PreparedStatement
public interface PreparedStatement extends Statement
代表的是一个预编译的SQL语句,它提供的setter方法可以传入查询的变量。
由于PreparedStatement是预编译的,通过它可以将对应的SQL语句高效的执行多次。由于PreparedStatement自动对特殊字符转义,避免了SQL注入攻击,因此应当尽量使用它。
PreparedStatement中注入null值
使用setNull方法把null值绑定到指定的变量上,setNull方法需要传入参数的所以及SQL字段的类型(i.e. java.sql.Types.VARCHAR)。
executeQuery无参数
执行SQL查询语句,返回ResultSet(never null)。
executeUpdate无参数
执行insert,update,delete语句(DML),或者什么也不返回的DDL语句。
如果时DML语句的话,返回更新的条数;如果时DDL语句的话,返回0.
PreparedStatement的优点
- 有助于防止SQL注入,因此它会自动对特殊字符转移;
- 可以用来进行动态查询;
- 执行更快,尤其当重用它或者使用它的拼量查询接口执行多条语句时;
- 使用setter方法更容易写出面向对象的代码;
PreparedStatement的缺点
不能直接用它来执行in条件语句。
解决办法参见: http://www.journaldev.com/2521/jdbc-preparedstatement-in-clause-alternative-approaches#dynamic-prepared-statement
执行SQL存储过程 - CallableStatement
通过JDBC的CallbleStatement接口来在数据库中执行存储过程。
参考:http://www.journaldev.com/2502/jdbc-callablestatement-stored-procedure-in-out-oracle-struct-cursor-example-tutorial
JDBC批处理
JDBC提供了批处理特性,可以在一次数据库调用中执行多条查询语句。
JDBC通过Statement和PreparedStatement中的addBatch和executeBatch方法来支持批处理。
参考: http://www.journaldev.com/2494/jdbc-batch-processing-example-tutorial-with-insert-statements
查询结果集ResultSet
在查询数据库后会返回一个ResultSet,它就像查询结果集的一张数据表。
ResultSet对象维护了一个游标,指向当前的数据行。开始的时候这个游标指向的是第一行。如果调用了ResultSet的next方法游标会下移一行,如果没有更多的数据了,next方法会返回false。可以在for循环中用它来遍历数据集。
默认的ResultSet是不能更新的,游标也只能往下移,也就是说只能从第一行到最后一行遍历一遍。不过也可以创建可以回滚或者可更新的ResultSet,例如:
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
当生成ResultSet的Statement对象关闭或者重新执行或是获取下一个ResultSet的时候,ResultSet对象也会自动关闭。
可以通过ResultSet的getter方法,传入列名或者从1开始的序号来获取列数据。
不同类型的ResultSet
根据创建 Statement时输入参数的不同,会对应不同类型的ResultSet,以支持不同的ResultSet和并发类型。
- ResultSet.TYPE_FORWARD_ONLY:默认的类型,游标只能往下移;
- ResultSet.TYPE_SCROLL_INSENSITIVE:游标可以上下移动,一旦它创建后,数据库里的数据再发生修改,对它来说时透明的;
- ResultSet.TYPE_SCROLL_SENSITIVE:游标可以上下移动,如果生成后数据库还发生了修改操作,它是能够感知到的;
ResultSet有两种并发类型:
- ResultSet.CONCUR_READ_ONLY:ResultSet是只读的,这是默认类型;
- ResultSet.CONCUR_UPDATABLE:我们可以使用ResultSet的更新方法来更新里面的数据;
查询结果集RowSet - 行集
public interface RowSet extends ResultSet (javax.sql)
RowSet用户存储查询的数据结果,和ResultSet相比,更灵活。
RowSet的特性:
- 提供了Java Bean功能
- RowSet对象默认时可滚动的,可更新的,因此,如果数据库系统不支持ResultSet实现类似功能,可以使用RowSet使用
RowSet分为:
- 连接型RowSet:这类对象与数据库进行连接,和ResultSet类似;JDBC接口只提供了一种连接型RowSet, JdbcRowSet
- 离线型RowSet:这类对象不需要和数据库进行连接,因此它们更轻量级,更容易序列化。适用于在网络间传递数据。
四种不同类型的RowSet:
- CachedRowSet:
- WebRowSet:
- JoinRowSet:
- FilteredRowSet:
RowSet V.S. ResultSet
RowSet继承自ResultSet.RowSet一个最大的好处是它可以是离线的,这样使得它更轻量级,同时便于在网络间进行传输。
具体使用哪个取决于需求,不过如果操作ResultSet对象的事件较长的话,最好选择一个离线的RowSet,这样可以释放数据库连接。
JDBC事务管理
默认情况下,创建的数据库连接(Connection),是在自动提交的模式下的。这意味着只要我们执行完一条查询语句,就会自动进行提交。因此我们的每条查询,实际上都是一个事物,如果我们执行的时DML或者DDL,每条语句完成的时候,数据库就已经完成修改了。
有时候我们希望又一组SQL查询组成一个事务,如果中途出现一场,可以进行回滚。
Connection的setAutoCommit方法,可以用来关闭自动提交的特性。应该在需要手动提交时才关闭这个特性。
数据库通过表锁来管理事务,这个操作非常消耗资源。因此应当完成操作后尽快的提交事务。
参考: http://www.journaldev.com/2483/jdbc-transaction-management-and-savepoint-example-tutorial
事务回滚
通过Connect的rollback方法可以回滚事务。它会回滚这次事务中的所有修改操作,并释放当前连接所持有的数据库锁。
Savepoint保存点
有时候事务保存了一组语句,我们希望回滚到这个事务的某个特定的点。JDBC保存点可以用来生成事务的一个检查点,使得事务可以回滚到这个检查点。
一旦事务提交或者回滚了,它生成的任何保存点都会自动释放并失效。回滚事务到某个特定的保存点后,这个保存点后所有其他的保存点会自动释放并且失效。
参考: http://www.journaldev.com/2483/jdbc-transaction-management-and-savepoint-example-tutorial
数据源DataSource
是定义在javax.sql中的一个接口,跟DriverManager相比,它的功能要更强大。可以使用它来创建数据库连接,驱动的实现类会实际去完成这个工作。
除了创建连接外,它还提供了如下的特性:
- 缓存PreparedStatement以便更快的执行;
- 可以设置连接超时事件;
- 提供日志记录的功能;
- ResultSet大小的最大阈值设置;
- 通过JNDI的支持,可以为servlet容器提供连接池的功能;
JDBC DataSource的示例参考: http://www.journaldev.com/2509/jdbc-datasource-example-oracle-mysql-and-apache-dbcp-tutorial
在Servlet容器中设置连接池
参考:http://www.journaldev.com/2513/tomcat-datasource-jndi-example-for-servlet-web-application
Apache的DBCP
如果用DataSource来获取连接的话,通常获取连接的代码和驱动特定的DataSource是紧耦合的。另外,除了选择DataSource的实现类,剩下的代码基本都是一样的。
Apache的DBCP(commons-pool)用来解决这些问题,它提供的DataSource实现成为了应用程序和不同JDBC驱动间的一个抽象层。
参考:http://www.journaldev.com/2509/jdbc-datasource-example-oracle-mysql-and-apache-dbcp-tutorial
其他
JDBC数据库隔离级别
事物隔离级别的前提是一个多用户,多线程,多进程的并发系统,在这个系统中为了保证数据的一致性和完整性,引入了事务隔离级别的概念。对一个单用户,单线程的应用不存在这个问题。
高并发系统中存在的问题
脏读
一个事务读到另一个事务还没有提交的数据,称之为脏读。
当我们使用事务时,有可能出现这样的情况,有一行数据刚更新,与此同时另一个查询读到了这个刚更新的值,这样就导致了脏读,因为更新的数据还没有进行持久化,更新这行数据的业务可能会进行回滚,这样这个数据就是无效的。
解决办法:如果在第一个事务提交前,任何其他事物不可读取其修改过的值,则可以避免该问题。
不可重复读
一个事务先后读取同一条记录,但两次读取的数据不同,我们称之为不可重复读。
解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。
幻读
一个事务先后读取一个范围的记录,但两次读取的记录数不同,称之为幻读。
解决办法:如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可以避免该问题。
不可重复读 V.S. 幻读
不可重复读的重点是修改:同样的提交,读取过的数据,再次读取出来发现值不一样了。
幻读的重点在于新增或者删除:同样的条件,第一次和第二次读出来的记录数不一样。
从控制的角度看:
不可重复读:只需要锁住满足条件的记录。
幻读:要锁住满足条件及其相近的记录。
参考:http://jianfulove.iteye.com/blog/1843963
JDBC隔离级别
当使用事务时,数据库系统用锁来防止别人访问事务中用到的数据。数据库通过锁来防止脏读,不可重复读及幻读的问题。
数据库使用JDBC设置的隔离级别来决定它使用何种锁机制。我们可以通过Connection的getTransactionIsolation和setTransactionIsolation方法来获取和设置数据库的隔离级别。
隔离级别 | 事务 | 脏读 | 不可重复读 | 幻读 |
TRANSACTION_NONE | 不支持 | 不可用 | 不可用 | 不可用 |
TRANSACTION_READ_COMMITTED | 支持 | 阻止 | 允许 | 允许 |
TRANSACTION_READ_UNCOMMITTED | 支持 | 允许 | 允许 | 允许 |
TRANSACTION_REPEATABLE_READ | 支持 | 阻止 | 阻止 | 允许 |
TRANSACTION_SERIALIZABLE | 支持 | 阻止 | 阻止 | 阻止 |
java.sql.Connection:
Modifier and Type | Field and Description |
---|---|
static int | TRANSACTION_NONE
A constant indicating that transactions are not supported.
|
static int | TRANSACTION_READ_COMMITTED
A constant indicating that dirty reads are prevented; non-repeatable reads and phantom reads can occur.
|
static int | TRANSACTION_READ_UNCOMMITTED
A constant indicating that dirty reads, non-repeatable reads and phantom reads can occur.
|
static int | TRANSACTION_REPEATABLE_READ
A constant indicating that dirty reads and non-repeatable reads are prevented; phantom reads can occur.
|
static int | TRANSACTION_SERIALIZABLE
A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented.
|
JDBC异常
- java.sql.SQLException - JDBC异常的基类;
- java.sql.BatchUpdateException - 当批处理操作执行失败的时候可能会抛出这个一场,这取决于具体的JDBC驱动的实现,它也可能直接抛出异常基类SQLException;
- java.sql.SQLWarning - SQL操作出现的警告信息;
- java.sql.DataTrunction - 字段值由于某些非正常原因被截断了 (不是因为超过对应字段类型的长度限制而被截断的);
SQLWarning
SQLWarning是SQLException的自雷,通过Connection,Statement,ResultSet的getWarnings方法都可以获取到它。SQLWarning不会中断查询语句的执行,只是用来提示用户存在相关的警告信息。
JDBC中的其他数据类型
CLOB
public interface Clob (java.sql)
Character Large Object字符大对象;它是由单字节字符组成的字符串数据,有自己专门的代码页。这种数据类型适用于存储超长的文本信息,那么可能会超出标准的VARCHAR类型长度限制的文本。
BLOB
public interface Blob (java.sql)
Binary Large Object二进制大对象;由二进制数据组成,没有专门的代码页。它能用于存储超过VARBINARY限制的二进制数据。这种类型适合存储图片,声音,图形,或者其他业务程序特定的数据。
Date
public class Timestamp extends Date (java.sql)
Date只存储日期数据不存储时间数据。
Timestamp
总结
- 数据库资源是非常昂贵的,用完了应该尽快关闭它。Connection,Statement,ResultSet等JDBC对象都有close方法;
- 养成在代码中显式关闭掉ResultSet,Statement,Connection的习惯;如果使用的是连接池的话,连接用完后会放回池里,但是没有关闭的ResultSet和Statement就会造成资源泄漏;
- 在finally块中关闭资源,保证即便出现了异常也能正常关闭;
- 大量类似的查询应当使用批处理完成;
- 尽量使用PreparedStatement而不是Statement,以避免SQL注入,同时还能通过预编译和缓存机制提升执行的效率;
- 如果要讲大量数据读入到ResultSet中,应该合理的设置fetchSize以便提升性能;
- 检查使用数据所支持的隔离级别;
- 数据库隔离级别越高性能越差,确保使用的数据库连接设置级别是最优的;
- 如果需要长时间对ResultSet进行操作的话,尽量使用离线的RowSet;