Java入门指南之数据库操作
概念
- JDBC
JDBC: Java DataBase Connectivity, 是Java语言中用于数据库操作的一个 API,JDBC是典型使用门面模式的用例。使用JDBC可以重用代码来访问各种类型的数据库,而不用针对每种数据库都写一个数据库操作类。当出现新型的数据库时,只要该数据库的生产商提供相应的JDBC驱动程序,就能使用原有代码对该新型数据库进行操作。
数据库基本操作步骤
- 取得数据库连接
- 1.载入驱动程序
Class.forName("驱动程序名称");
例如,使用Sun公司提供的JDBC桥接驱动程序,该驱动程序的名称为“sun.jdbc.odbc.JdbcOdbcDriver”,就该使用下面语句来加载驱动程序:
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
- 2.建立连接
Connect con = DriverManager.getConnection(url,"用户名","用户密码");
其中,url表示数据库统一资源定位的一个字符串,其语法常为:
如:jdbc:subprotorol:subname //JDBC:子协议:副名
String url="jdbc:microsoft:sqlserver://localhost";
下面是一个拓展,大家可以看看。
如果驱动程序和URL都被“硬”编程到程序应用中,一旦访问的数据库类型改变,那么就需要修改代码并重新编译运行,这不仅仅不方便操作,还削弱了JDBC数据库编程独立于数据库类型的优点。那么,该怎么办呢? 我们可以使用**配置文件**的方式在解决这个问题:将驱动程序和数据库URL等信息保存在一个配置文件里,应用程序进行数据库连接时,从配置文件中读取信息,这样就能提高应用程序的灵活性和可扩展性。 如下,我们可以在应用程序所在目录中创建这样一个配置文件db.cfg
Java语言中提供了一个类**java.util.Properties**,该类提供了load()方法,可以从输入流中读取属性值,下面的代码实现的是从配置文件db.cfg中读入配置信息放到对象prop中:dbDriver = com.microsoft.jdbc.sqlserver.SQLServerDriver
dbIP = 1227.0.0.1
dbPort = 1433
dbName = admin
dbPassWord = pwd
defaultDbName = dataBaseName
Properties prop = new Properties();
prop.load(new FileInputStream("db.cfg"));
要注意的是,从配置文件中读取的信息是以**键值对**的形式存放在prop对象中的,例如,要获取配置文件中dbDriver的属性值,可以使用**getProperty()方法**:
String driver = porp.getProperty("dbDriver");
在实际应用中,我们可以提供一个界面,让用户在该界面中设置数据库参数,比如数据库驱动名称、数据库URL等,然后把这些参数信息保存在一个配置文件中,操作数据库时需要的代码参数就从这个配置文件中读取。
2.执行SQL语句
SQL语句用来表明要对数据库进行的操作。
下面介绍两种向数据库发送SQL语句的方法。
一种是使用连接对象(Connection)中的creatStatement()方法创建一个Statement对象,然后通过Statement对象对SQL语句进行编译执行。
另一种是通过数据库连接对象(Connection)创建PrepareStatement类型的对象,然后通过PrepareStatement对象向DBMS发送SQL语句进行预编译,再执行。
2. 1. 使用Statement对象
我们可以使用下面语句来获得一个Statement对象:
Statement stmt = con.createStatement();
Statement类提供了几种不同的执行的SQL语句的方法,主要有:
- executeUpdate(SQL): 这个方法用来执行那些会修改数据库的SQL语句,例如insert,delete,updare以及create等命令。假设用stmt对象向数据库bookDateBase的bookInfo表中插入一条记录,可以采用下面语句:
stmt.executeUpdate("insert into bookInfo values('ISBN-B0002','Java DataBase Operation',127.0)");
- executeQuery(SQL):这个方法用于对数据库的查询操作,该方法返回一个ResultSet类型的对象,该对象包含了所有查询结果,如:
ResultSet rs = stmt.executeQuery("select * from bookInfo");
如果要访问结果集中的一条记录,需要定位到该记录。ResultSet类提供了next()方法用于依次定位结果集中的每条记录;还提供了getXXX()方法用于访问当前记录中字段的值,使用getXXX()方法必须在方法参数中指明所访问字段的列索引或是列名。如:
String name = rs.getString(2); //列索引
或
String name = rs.getString("bookName"); //列名
注意,与数组下标索引不同,上面的列索引是从1开始的。
另外,如果执行的是以下查询:
ResultSet rs = stmt.executeQuery("select bookName from bookInfo");
要用列索引的方式查询“bookName”字段,就应该使用:
String name = rs.getString(1); //索引不再是2
因为字段的列索引指的是在ResultSet返回集中所在列的位置,而不是在数据库中列中的位置,上面语句的返回集中只有“bookName”这一列,所以列索引为1。
如果要处理返回的记录集(ResultSet对象),可以使用下面的代码段:
while(rs.next()){
//使用列名方式
System.out.println("列名方式-字段内容为:"+rs.getString("bookName"));
//使用列索引方式:
System.out.println("列索引方式-字段内容为:"+rs.getString(1));
}
- executeBatch(SQL): 这个方法用来批量执行SQL语句。需要注意的是,这些要批量执行的语句是更新类型(既是insert、delete、delete以及create等命令)的,并且其中不能包含查询类型(selsect)的SQL语句。
下面代码演示了executeBatch()方法的使用:
stmt.addBatch(updateSql_1);
stmt.addBatch(updateSql_2);
stmt.addBatch(updateSql_3);
int [] results = stmt.executeBatch();
该方法返回的是一个整型数组,其中依次存放了每条SQL语句对数据库中产生影响的行序号。
2. 2. 使用PrepareStatement对象
使用这个方法操作数据库时,先要写一条操作数据库的SQL语句,并将其作为参数传入创建方法中,这条作为参数的SQL语句中允许使用参数占位符“ ?”。使用参数占位符的好处是:每次运行这条SQL语句时,可以通过赋给参数占位符不同的参数值,从而完成不同的功能。获取一个PrepareStatement对象可以用下列语句:
PreparedStatement pstmt = con.prepareStatement(sql_statement);
其中,sql_statement 这个参数是要执行的SQL语句,它会立即被发送到DBMS(数据库管理系统)进行预编译。
下面我们假设要向表bookInfo中插入一条新记录(’ISBN-G006’,’Java Data Structure’,196.8),来写一段代码演示PreparedStatement的使用:
sql = " insert into bookInfo values(?,?,?) ";
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.setString(1,"ISBN-G006"); //对应values的第一个占位符“?”
pstmt.setString(2,"Java Data Structure");//对应values的第二个占位符“?”
pstmt.setString(3,196.8);//对应values的第三个占位符“?”
pstmt.executeUpdate();
在实际情况中,往往一条记录有很多个字段,那么使用setXXX()的方式来完成的话,代码量就会比较大,效率不高。这种情况下,我们可以使用setObject()方法结合循环语句来完成。同样,以上面假设的情景为例,我们可以这样写:
sql = “ insert into bookInfo values(?,?,?) ”;
PreparedStatement pstmt = con.prepareStatement(sql);
Object [] record = {"ISBN-G006","Java Data Structure",new Float(196.8)};
for(int i = 1;i<record.length;i++){
pstmt.setObject(i,record[i-1]);
}
pstmt.executeUpdate();
与Statement相比,PrepareStatement有下列优点:
- PreparedStatement是预编译方式执行的,其中的SQL语句参数会先被传到DBMS预编译,所以当其执行时,只需DBMS运行SQL语句,效率会比较高;
- PreparedStatement中的SQL语句是可以带参数的,它的优势在于可以减少拼接SQL语时句出错的几率,容易阅读,方便维护。同时,还能减少SQL注入攻击的可能性,增加SQL的安全性;
- 当批量处理SQL语句或频繁执行相同的查询时,PreparedStatement有明显的性能上的优势,由于数据库可以将编译优化后的SQL语句缓存起来,下次执行相同结构的语句时不用再次编译,效率就会比较快。
3.关闭连接,释放资源
数据库操作完成后,需要依次将ResultSet、Statement或PreparedStatement、Connection对象关闭,以释放所占用的资源,可以使用这些代码来执行:
if(rs != null){ // 关闭记录集
try{
rs.close() ;
}catch(SQLException e){
e.printStackTrace() ;
}
}
if(stmt != null){ // 关闭声明
try{
stmt.close() ;
}catch(SQLException e){
e.printStackTrace() ;
}
}
if(con != null){ // 关闭连接对象
try{
con.close() ;
}catch(SQLException e){
e.printStackTrace() ;
}
}
数据库事务
什么是数据库事务?
通俗点来说,我们要对数据库进行几个操作(增删改查等)的组合,以完成一个完整的事件。比如,数据库的记录A、记录B分别代表两个银行账户,其中都有资金的这一字段。现在要将账户A的部分资金转给账户B,那么就存在A账户减去部分资金,B账户加上部分资金这两个操作的组合,以完成转账事件。如果任意一步操作出错,这一系列的操作过程就必须重新来过。比如A的资金减少了,但是B的增加资金的操作出错而没有得到转账资金,那么这整个转账事件就必须就回到初始状态,重新执行。一组数据库操作步骤要么全部发生要么一步也不执行。这整个执行过程称之为一个事务。
当所有的步骤被完整地执行,我们称该事务被提交。如果由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。
数据库事务有四大特性(简称ACID原则):
- 原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,要么均不执行。
- 一致性(Consistency):指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿上面转账情景来说,假设账户A和账户B两者的资金加起来一共是10000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是10000,这就是事务的一致性。 - 隔离性(Isolation):事务的执行不受其他事务的干扰,多个并发事务之间要相互隔离,事务执行的中间结果对其他事务必须是不可见的。
- 持久性(Durability):持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。持久性通过数据库备份和恢复来保证。
JDBC 事务是用 Connection 对象控制的。JDBC Connection 接口( java.sql.Connection )提供了两种事务模式:自动提交和手动提交。 java.sql.Connection 提供了以下控制事务的方法:
public void setAutoCommit(boolean)
public boolean getAutoCommit()
public void commit()
public void rollback()
JDBC 事务的范围局限于一个数据库连接。一个 JDBC 事务不能跨越多个数据库。
对于上面的四个方法,getAutoCommit() 是用来获取是否是自动提交模式的布尔值,若是则返回true。
根据实际情况说一下其他三个方法的应用。
默认情况下,创建的连接是处于自动提交(AutoCommit)模式的,每条SQL语句执行后,会立即向DBMS提交执行结果,也就是说每条SQL语句都是一个事务。为了完成若干条语句完成的事务,要在执行第一条SQL语句前关闭自动提交模式:
con.setAutoCommit(false);
关闭自动提交模式后,所执行的SQL语句不会直接提交给DBMS,直到调用如下语句进行手动提交:
con.commit();
为实现事务步骤出错后能恢复至原始状态。我们可以把一个事务放在一个try块中,当执行过程有SQL语句出现错误时,我们可以在catch块中将其捕获,在catch块中调用rollBack()方法进行事务回滚,恢复到事务开始时的状态。
由于事务的回滚仍可能出现异常,因此同样需要使用try-catch块来捕获回滚异常。
下面是执行一个数据库事务的代码:
try{
con.setAutoCommit(false); //设置为非自动提交模式
PreparedStatement pstmt_1 = con.prepareStatement(sql_1);
PreparedStatement pstmt_2 = con.prepareStatement(sql_2);
pstmt_1.executeUpdate();
pstmt_2.executeUpdate();
con.commit(); //手动提交
con.setAutoCommit(true); //恢复自动提交模式
pstmt_1.close();
pstmt_2.close();
}catch(SQLException e){
e.printStackTrace();
if(con != null){
try{
con.rollback();
con.setAutoCommit(true);
}catch(SQLException ex){
ex.printStackTrace();
}
} //End if
}
数据库连接池
最后来说一说数据库连接池(Connection Pool),首先说说连接池的概念。
在实际应用中,可能会频繁地操作数据库,那么就需要频繁地与数据库建立连接,然后执行SQL语句,最后再关闭资源。我们知道,数据库的连接期间系统要分配内存资源,而且,数据库连接是一个占用时间比较多的操作。一个网站可能同时有成百上千人在线,如果这些用户需要频繁地访问数据库,那么在没有使用连接池的情况下,系统可能会不停地创建连接,分配系统资源,连接数据库的操作过多时,就可能出现系统崩溃的情况。
这时,数据库连接池技术就能很好地解决这个问题。我们建立一个用来存放数据库连接的缓冲池。预先在这个缓冲池中放入一定数量的连接,当我们需要连接数据库时,并不需要建立一个新的连接,而是从这个缓冲池里取出一个数据库连接,使用完毕后再放回缓冲池而无需关闭连接。
数据库连接池通过建立一个数据库连接池以及一套连接使用、分配、管理方法,使得该连接池中的连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。
关于连接池的最小连接数和最大连接数:
- 最小连接数:即数据库连接池中一直保持的连接数量,如上文中预先放入缓冲池(即连接池)的连接数量,无论这些数据库连接是否被使用,连接池都将一直保证至少存放着这么多的连接数量。
- 最大连接数:最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。避免系统一时间产生大量连接,限制了系统分配的最大资源。
另外,还可以通过设置每个连接的最大使用次数、最大空闲时间等参数来管理连接池的使用情况。
关于数据库连接池的实现代码,我在无意中看到一位大神的实现思路,过程比较精彩,大家可以移步: 数据库连接池的实现
以上是个人学习过程中的关于Java数据库操作所作的笔记,参考了部分资料,希望对学习Java数据库的新手能有所帮助。如果大家发现文章中存在不当或错误之处,请大家批评指正。