JDBC(Java Data Base Connectivity)是Java程序员操作数据库的API,也是Java程序与数据库交互的一门技术。JDBC是Java操作数据库的规范,由一组用Java语言编写的类与接口组成,它对数据库的操作提供了基本方法,但对于数据库的细节操作由数据库厂商进行实现。JDBC对于Java程序员而言,是一套标准的操作数据库的API;而对数据库厂商而言,又是一套标准的模型接口。
JDBC最终是为了实现以下目标:
JDBC规范将驱动程序归结为以下几类:
连接数据库前,需要将数据库厂商提供的数据库驱动类注册到JDBC的驱动管理器,通常情况下,是通过将数据库驱动加载JVM来实现的。
//Class的forName方法作用是将指定字符串名的类加载到JVM中。在加载后,数据库驱动程序会把驱动类自动注册到驱动管理器
Class.forName("com.mysql.jdbc.Driver");
2.构建数据库连接URL
要建立数据库连接,就要构建数据库连接的URL,这个URL由数据库厂商指定,不同的数据库,它的URL也有所区别,但都符合一个基本的格式,即“JDBC协议+IP地址或域名+端口+数据库名称”。如MySQL的数据库连接URL的字符串为“jdbc:mysql://localhost"3306/test”。
在注册了数据库驱动及构建数据库URL后,就可以通过驱动管理器获取数据库连接Connection。Connection对象是JDBC封装的数据库连接对象,只有创建此对象后,才可以对数据进行相关操作。获取方法如下:
//Connection对象的获取需要用到DriverManager对象,DriverManager的getConnection方法通过数据库连接URL,数据库用户名和数据库密码创建Connection对象
DriverManager.getConnection(url,username,password);
注意:在JDK中,不包含数据库的驱动程序,使用JDBC操作数据库,需要事先下载数据库厂商提供的驱动包。例如使用MySQL数据库,就要添加MySQL官方提供的数据库驱动包。
JDBC是Java操作数据库的规范,由一组用Java语言编写的类与接口组成,也就是JDBC API。
Connection接口位于java.sql包中,是与特定数据库的连接会话,只有获得特定数据库的连接对象,才能访问数据库,操作数据库中的数据表,视图和存储过程等。Connection接口的常用方法声明及说明如下:
方法声明 | 说明 |
void close() throws SQLException | 立即释放Connection对象的数据库连接占用的JDBC资源,在操作数据库后,立即调用该方法 |
void commit() throws SQLException | 提交事务,并释放Connection对象当前持有的所有数据库锁。当事务被设置为手动提交模式时,需要使用该方法提交事务 |
Statement createStatement() throws SQLException | 创建一个Statement对象来讲SQL语句发送到数据库,该方法返回Statement对象 |
PreparedStatement preparedStatement(String sql) throws SQLException | 将参数化的SQL语句预编译并存储在PreparedStatement对象中,并返回创建的对象。 |
DatabaseMetaData getMetaData() throws SQLException | 获取Connection对象所连接的数据库的元数据DataBaseMetaData对象,元数据包括关于数据库的表,受支持的SQL语法,存储过程,此连接功能等信息。 |
boolean isClosed() throws SQLException | 判断Connection对象是否与数据库断开连接,该方法返回布尔值。需要注意的是,如果断开连接,则不能再使用Connection对象操作数据库。 |
void rollback() throws SQLException | 回滚事务,并释放Connection对象持有的所有数据库锁,注意该方法需要应用于Connection对象的手动提交模式。 |
使用JDBC操作数据库,需要使用数据库厂商提供的驱动程序,通过驱动程序可以与数据库进行交互。DriverManager类主要作用于用户及驱动程序之间,它是JDBC中的管理层,通过DriverManager类可以管理数据库厂商提供的驱动程序,并建立应用程序与数据库之间的连接,其方法声明及说明如下:
方法声明 | 说明 |
public static void deregisterDriver(Driver driver) throws SQLException | 从DriverManager的管理列表中删除一个驱动程序,参数driver为要删除的驱动对象。 |
public static Connection getConnection(String url) throws SQLException | 根据指定数据库连接URL,建立与数据库连接Connection。参数url为数据库连接URL |
public static Connection getConnection(String url,Properties info) throws SQLException | 根据指定数据库连接URL及数据连接属性信息建立与数据库连接Connection。 |
public static Connection getConnection(String url,String user,String password) throws SQLException | 根据指定数据库连接URL,用户名和密码建立与数据库连接Connection。参数url为数据库连接URL |
public static Enumeration |
获取当前DriverManager中已经加载的驱动程序。 |
public static void registerDriver(Driver driver) throws SQLException | 向DriverManager注册一个驱动对象。 |
在创建数据库连接之后,就可以通过程序来调用SQL语句对数据库进行操作,在JDBC中Statement接口封装这些操作。Statement接口提供了执行语句和获取结果的基本方法。
方法声明 | 说明 |
void addBatch() throws SQLException | 将SQL语句添加到Statement对象的当前命令行列表中,该方法用于SQL命令的批处理。 |
void clearBatch() throws SQLException | 清空Statement对象中的命令列表。 |
void close() throws SQLException | 立即释放Statement对象的数据库和JDBC资源。 |
boolean execute(String sql) throws SQLException | 执行指定的SQL语句。如果SQL语句返回结果,该方法返回true。 |
int[] executeBatch() throws SQLException | 将一批命令提交给数据库执行,返回更新计数组成的数组。 |
ResultSet executeQuery(String sql) throws SQLException | 执行查询类型的SQL语句 |
Connection getConnection() throws SQLException | 获取生成Statement对象的Connection对象 |
boolean isClosed() throws SQLException | 判断Statement对象是否被关闭。 |
PreparedStatement接口继承与Statement接口,拥有Statement接口中的方法,而且PreparedStatement接口针对带参数SQL语句的执行操作进行了扩展。应用于PreparedStatement接口中的SQL语句,可以使用占位符?来代替SQL语句中的参数,然后再对其进行赋值。常用方法如下:
方法声明 | 说明 |
void setInt(int parameterIndex,int x) throws SQLException | 将int值x作为SQL语句中的参数值,parameterIndex作为参数位置的索引 |
...(float double类型和setInt类似) |
执行SQL语句的查询语句会返回查询的结果集,在JDBC API中,使用ResultSet对象接收查询结果集。
ResultSet接口位于java.sql包中,封装了数据查询的结果集。ResultSet对象包含了符合SQL语句的所有行,,针对Java中的数据类型提供了一套getXXX()方法,通过这些方法可以获取每一行中的数据。ResultSet还提供光标的功能,通过光标可以自由定位到某一行中的数据。常用方法如下:
方法声明 | 说明 |
boolean absolute(int row) throws SQLException | 将光标移动到ResultSet对象的给定行编号 |
void afterLast() throws SQLException | 将光标移动到ResultSet对象的最后一行之后,如果结果集中不包含任何行,该方法无效 |
InputStream getBinaryStream(String columnLabel) throws SQLException | 以byte流的方法获取ResultSet对象当前行的指定列的值 |
int getInt(Strinf columnLabel)throws SQLException | 以int的方式获取ResultSet对象当前行中的指定列的值 |
boolean previous() throws SQLException | 将光标位置向前移动一行 |
添加数据:通过JDBC向数据库添加数据,可以使用insert语句实现插入数据的SQL语句,对于SQL语句中的参数可以使用占位符?代替,然后通过PreparedStatement对其赋值并执行SQL(在执行预备语句之前,必须使用set方法将变量绑定到实际值上。使用PreparedStatement对象对SQL语句的占位符参数赋值,其参数的下标值不是0,而是1)。进行赋值后,需要调用executeUpdate方执行操作,更新数据库中的信息。
查询数据:查询的过程与添加的流程基本相同,但执行查询数据操作后需要通过一个对象装载查询结果集,这个对象就是ResultSet对象。ResultSet集合所查询的数据位于集合的中间位置,在第一条数据之前和最后一条数据之后都有一个位置。默认情况下,ResultSet的光标位置在第一行数据之前,所以在第一次获取数据就需要移动光标位置。
修改数据:操作方法与添加数据类似,使用update语句实现。在实际开发中,通常情况下都是由程序传递SQL语句中的参数,所以修改数据需要使用PreparedStatement对象进行操作。
删除数据:操作方法与添加数据类似,使用delete语句实现。在实际开发中,通常情况下都是由程序传递SQL语句中的参数,删除数据需要使用PreparedStatement对象进行操作。
在SQL中,二进制大对象称为BLOB,字符型大对象称为CLOB。
要读取LOB,需要执行SELECT语句,然后ResultSet上调用getBlob或getClob方法,这样就可以获得Blob或Clob类型的对象。要从Blob中获取二进制数据,可以调用getBytes或getBinaStram。要从Clob中获取其中的字符数据,可以调用getSubString或getCharacterStream。
要将LOB置于数据库中,需要在Connection对象上调用createBlob或createClob,然后获取一个用于该LOB的输出流或写出器,写出数据,并将该对象存储到数据库中。
java.sql.Blob的方法如下:
方法声明 | 说明 |
long length() | 获取该BLOB的长度 |
byte[] getBytes(long startPosition,long length) | 获取该BLOB中的给定范围的数据 |
InputStream getBinaryStream() InputStream getBinaryStream(long startPosition,long length) |
返回一个输入流,用于读取该BLOB中全部或给定范围的数据 |
OutputStream setBinaryStream(long startPosition) | 返回一个输出流,用于从给定位置开始写入该BLOB |
java.sql.Clob
方法声明 | 说明 |
long length() | 获取该CLOB中的字符总数 |
String getSubString(long startPosition,long length) | 获取该CLOB中给定范围的字符 |
Reader getCharacterStream() Reader getCharacterStream(long startPosition,long length) |
返回一个读入器,用于读取该CLOB中全部或给定范围的数据 |
Writer setCharacterStream(long startPosition) | 返回一个写出器,用于从给定位置开始写入该CLOB |
在执行存储过程,或者在使用允许单个查询中提交多个select语句的数据库时,一个查询有可能返回多个结果集。下面是获取所有结果集的步骤:
(1)使用execute方法执行SQL语句。
(2)获取第一个结果集或更新计数。
(3)重复调用getMoreResults方法以移动下一个结果集。
(4)当不存在更多的结果集或更新计数时,完成操作。
如果由多结果集的链中的下一项是结果集,execute和getMoreResults方法将返回true,而如果在链中的下一项不是更新计数,getUpdateCount方法将返回-1。
JDBC没有提供独立于提供商的自动生成建的解决方法,但是它提供了获取自动生成建的有效途径。当向数据表中插入一个新行,且其键自动生成时,可以用下面的代码获取这个键:
stat.executeUpadate(insertStatement,Statement.RETURN_GENERATED_KEYS);
ResultSet rs=stat.getGeneratedKeys();
if(rs.next)
{
int key=rs.getInt(1);
...
}
默认情况下,结果集是不可滚动进而不可更新的。为了从查询中获取可滚动的结果集,必须使用下面的方法得到一个不同的Statement对象:
Statement stat=conn.createStatement(type,concurrency);
如果要获得预备语句,请调用下面的方法:
PreparedStatement stat=conn.preparedStatement(command,type,concurrency);
type和concurrency参数都是有关于结果集的取值。
ResultSet类的type值:
值 | 解释 |
TYPE_FORWARD_ONLY | 结果集不能滚动(默认值) |
TYPE_SCROLL_INSENSITIVE | 结果集可以滚动,但是对数据库变化不敏感 |
TYPE_SCROLL_SENSITIVE | 结果集可以滚动,且对数据库变化敏感 |
ResultSet的Concurrency值:
值 | 解释 |
CONCUR_READ_ONLY | 结果集不能用于更新数据库(默认值) |
CONCUR_UPDATABLE | 结果集可以用于更新数据库 |
并非所有的数据库驱动程序都支持可滚动和可更新的结果集。使用DataBaseMetaData接口中的supportResultSetType和supportResultSetConcurrency方法,可以获知在使用特定的驱动程序时,某个数据库究竟支持哪些结果集类型和哪些并发模式。即便数据库支持所有的结果集模式,某个特定的查询也可能无法产生带有所请求的所有属性的结果集(例如,一个复杂查询的结果集就有可能是不可更新的结果集)在这种情况下,executeQuery方法将返回一个功能较少的ResultSet对象,并添加一个SQLWarning到连接对象中。或者,也可以使用ResultSet接口中的getType和getConcurrency方法查看结果集实际支持的模式。如果不检查结果集的功能就发起一个不支持的操作,比如对不可滚动的结果集调用previous方法,程序会抛出SQLException异常。
如果希望编辑结果集中的数据,并且将结果集上的数据变更自动反应到数据库中,那么必须使用可更新的结果集。应该使用下面的方法创建一条语句:
Statement stat=conn.createStatement(Result.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATEABLE);
并非所有的查询都会返回可更新的结果集,如果查询涉及多个表的连接操作,那么它所产生的结果集将是不可更新的,如果查询只涉及一个表,或者在查询时是使用主键连接多个表,那么它产生的结果集将是可更新的结果集。可以调用ResultSet接口中的getConcurrency方法来确定结果集是否可更新。
要注意的是,updateXxx方法改变的是结果集中的行值,而非数据库中的值。当更新完行中的字段值后,必须调用updateRow方法,这个方法将当前行中的所有更新信息发送给数据库。还可以调用cancelRowUpdates方法来取消对当前行的更新。
可滚动的结果集虽然功能强大,但是有一个重要的缺陷:在于用户交互的过程中,始终与数据库保持连接。RowSet接口扩展自ResultSet接口,却无需始终保持与数据库的连接。
构建行集:以下是javax.sql.rowset包提供的接口,扩展了RowSet接口。
被缓存的行集:包含了一个结果集中的所有数据。CachedRowSet是ResultSet接口的子接口,可以像使用结果集一样来使用行集。被缓存的行集有一个优点:断开数据库连接后仍然可以使用行集。在执行每个用户命令时,只需要打开数据库连接,执行查询操作,将查询结果放入被缓存的行集,然后关闭数据库连接即可。甚至可以修改被缓存的行集,必须发起一个显式的请求,以便让数据库真正接收所有修改。此时CachedRowSet类会重新连接到数据库,并通过执行SQL语句向数据库写入所有修改后的数据。
可以使用一个结果集来填充CachedRowSet对象。
ResultSet result=...;
RowSetFactory f=RowSetProvider.newFactory();
CachedRowSet c=f.createCachedRowSet();
crs.populate(result);
conn.close();
或者,也可以让CachedRowSet对象自动建立一个数据库连接。
ResultSet result=...;
RowSetFactory f=RowSetProvider.newFactory();
CachedRowSet c=f.createCachedRowSet();
//设置数据库参数
c.setURL("驱动");
c.setUsername("");
c.setPassword("");
//设置查询语句和所有参数
c.setCommand("查询语句");
c.setString(参数);
//将查询结果填充到行集
c.execute();
javax.sql.rowset.CachedRowSet常用的方法声明和说明如下:
方法声明 | 说明 |
void execute(Connection conn) | 通过执行使用setCommand方法设置的语句集来填充行集,该方法使用给定的连接,并负责关闭它。 |
void populate(ResultSet result) | 将指定的结果集中的数据填充到被缓存的行集中。 |
void acceptChanges(Connection conn) | 重新连接数据库,并写回行集中修改过的数据。如果因为数据库中的数据已经被修改而导致无法写回行集中的数据,该方法可能会抛出SyncProviderException异常。 |
默认情况下,数据库连接处于自动提交模式。每个SQL语句一旦执行便被提交到数据库。一旦命令被提交,就无法对它执行回滚操作。在使用事务时,需要关闭这个默认值:
conn.setAutoCommit(false);
使用事务的步骤:
//创建一个语句对象
Statement stat=conn.createStatement();
//任意多次调用executeUpdate方法
stat.executeUpdate(command1);
stat.executeUpdate(command2);
stat.executeUpdate(command3);
...
//执行所有命令没有出错,则调用commit方法
conn.commit();
//如果出现错误
conn.rollback();
在使用某些驱动程序时,使用保存点可以更细粒度地控制回滚操作。创建一个保存点意味着稍后只需返回这个点,而非事务的开头。
Statement stat=conn.createStatement();
stat.executeUpadate(command1);
Savepoint s=conn.setSavepoint();
stat.executeUpdate(command2);
if(...) connn.rollback(svpt);
...
conn.commot();
批量更新:一个语句序列作为一批操作将同时被收集和提交。使用DataBaseMetaData接口中的supportsBatchUpdates方法可以获知数据库是否支持这种特性。
处于同一批的语句可以是insert,update和delete语句。也可以是数据库定义语句。但是在批量处理中添加SELECT语句会抛出异常(从概念上讲,批量处理中的SELECT语句没有意义,因为它会返回结果集,并不更新数据库)。
操作步骤:
//创建一个语句对象
Statement stat=conn.createStatement();
//调用addBatch方法
String command="...";
stat.addBatch(command);
while(...)
{
command="...";
stat.addBatch(command);
}
//提交整个批量更新语句
int[] counts=stat.executeBatch();
//这个回滚的时候,记住关闭自动提交模式
在Java Web开发中使用JDBC,应遵循MVC的设计思想,从而使Web程序拥有一定的健壮性和可扩展性。JDBC处于MVC的模型层位置:
客户端通过JSP页面与程序进行交互,对于数据的增删改查请求由Servelt对其进行分发处理,如Servelt接收到添加数据请求,就会分发给增加数据的JavaBean对象,而真正的数据库操作是用过JDBC封装的JavaBean进行实现。
ResultSet是JDBC API中封装的查询结果集对象,通过该对象可以实现数据的分页显式。在ResultSet对象中,有一个“光标”的概念,光标通过上下移动定位查询结果集中的行,从而获取数据。所以通过Result的移动光标,可以设置ResultSet对象中的记录的起始位置和结束位置,来实现数据的分页显式。
通过ResultSet的光标实现分页,优点是在各种数据库上通用,缺点是占用大量资源,不适合数据量大的情况。
很多数据库自身提供了分页机制,如MySQL数据库中提供limit关键字。其优点是减少数据库资源的消耗,提高程序的性能;缺点是只针对某一数据库通用。
例子:通过MySQL数据库提供的分页机制,实现商品信息的分页查询功能,将分页数据显式在JSP页面中。
首先创建一个名为productbase的数据库,并且在其中建立一个表tb_product表,在这个表中添加数据。如图所示:
创建一个名称为Product的类,用于封装商品信息,该类时商品信息的JavaBean。代码如下:
package test;
public class Product {
public static final int PAGE_SIZE=2; //每页的记录数
private int id; //姓名
private String name; //名字
private double price; //价格
private int num; //数量
private String unit; //单位
public String getName() {
return name;
}
public double getPrice() {
return price;
}
public int getId() {
return id;
}
public int getNum() {
return num;
}
public String getUnit() {
return unit;
}
public void setName(String name) {
this.name = name;
}
public void setId(int id) {
this.id = id;
}
public void setNum(int num) {
this.num = num;
}
public void setPrice(double price) {
this.price = price;
}
public void setUnit(String unit) {
this.unit = unit;
}
}
创建名称为ProductDao的类,主要封装商品信息的数据库相关操作。代码如下:
package test;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class ProductDao {
/*
获取数据库连接
*/
public Connection getConnetion()
{
Connection conn=null;
try
{
Class.forName("com.mysql.jdbc.Driver"); //加载数据库驱动,注册到驱动管理器
String url="jdbc:mysql://localhost:3306/productbase?serverTimezone=UTC"; //数据库连接字符串
String name="root"; //数据库用户名
String password="password"; //数据库密码
conn= DriverManager.getConnection(url,name,password); //创建数据库连接
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return conn;
}
/*
分页查询所有商品信息
这个方法中使用limit关键字实现分页查询
limit arg1,agr2
arg1:用于指定查询记录的起始位置
arg2:用于指定查询数据所返回的记录数
*/
public List find(int page)
{
List list=new ArrayList();
Connection connection=getConnetion();
String sql="select * from tb_product order by id desc limit ?,?"; //分页查询SQL语句
try
{
PreparedStatement ps=connection.prepareStatement(sql);
ps.setInt(1,(page-1)* Product.PAGE_SIZE);
ps.setInt(2, Product.PAGE_SIZE);
ResultSet resultSet=ps.executeQuery();
while(resultSet.next())
{
Product p=new Product(); //赋值
p.setId(resultSet.getInt("id"));
p.setName(resultSet.getString("name"));
p.setPrice(resultSet.getDouble("price"));
p.setNum(resultSet.getInt("num"));
p.setUnit(resultSet.getString("unit"));
list.add(p);
}
resultSet.close();
ps.close();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
/*
查询总记录数
*/
public int findCount()
{
int count=0;
Connection connection=getConnetion();
String sql="select count(*) from tb_product";
try {
Statement statement=connection.createStatement();
ResultSet resultSet=statement.executeQuery(sql);
if(resultSet.next())
{
count=resultSet.getInt(1);
}
resultSet.close();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
return count;
}
}
创建名称为FindServelt的类,该类是分页查询商品信息的Servelt对象。代码如下:
package test;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@WebServlet(name = "FindServlet")
public class FindServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
/*
分别获取分页查询结果集和构造分页条对象
分页查询结果集调用ProductDao的find方法,并传递所查询的页码就可以获取
分页条对象时JSP页面中的分页条,用于显示商品信息的条码。在程序中通过创建页码的超链接,组合字符串进行构造
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int currPage=1;
if(request.getParameter("page")!=null)
{
currPage=Integer.parseInt(request.getParameter("page"));
}
ProductDao dao=new ProductDao();
List list=dao.find(currPage);
request.setAttribute("list",list);
int pages;
int count=dao.findCount();
if(count% Product.PAGE_SIZE==0)
{
pages=count/ Product.PAGE_SIZE;
}
else {
pages=count/ Product.PAGE_SIZE+1;
}
StringBuffer sb=new StringBuffer();
for(int i=1;i<=pages;i++)
{
if(i==currPage)
{
sb.append("["+i+"]");
}
else
{
sb.append(""+i+"");
}
sb.append(" ");
}
request.setAttribute("bar",sb.toString());
request.getRequestDispatcher("product_list.jsp").forward(request,response);
}
}
创建product_list.jsp页面,该页面通过获取结果集List与分页条来显示商品信息数据。代码如下:
<%@ page import="test.Product" %>
<%@ page import="java.util.List" %><%--
Created by IntelliJ IDEA.
User: Administrator
Date: 2019/11/16
Time: 11:08
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
商品信息
所有商品信息
ID
商品名称
价格
数量
单位
<%
List list=(List)request.getAttribute("list");
for(Product p:list){
%>
<%=p.getId()%>
<%=p.getName()%>
<%=p.getPrice()%>
<%=p.getNum()%>
<%=p.getUnit()%>
<%
}
%>
<%=request.getAttribute("bar")%>
编写程序中的主页面index.jsp,在该页面中编写分页查询商品信息的超链接,指向FindServelt。代码如下:
<%-- Created by IntelliJ IDEA. --%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
查看所有商品信息
完成代码的编写,部署运行项目,打开index页面:
点击页面的超链接,显示结果为: