Java访问数据库通过JDBC,JDBC是Java标准库定义接口,各数据库厂商以“驱动”的形式实现接口。应用程序要使用哪个数据库,就把该数据库厂商的驱动以jar包形式引入进来,同时自身仅使用JDBC接口,编译期并不需要特定厂商的驱动。
《Java JDK 9学习笔记》中使用的是SQLite,所以需要下载SQLite的JDBC驱动程序(如果是MySql的话则下载其对应的驱动类库),由于项目使用的是JDK9新增的模块化设计,对于下载的SQLite的驱动程序JAR的使用会有问题,其解决方式如下:
1、JDBC数据库编程的示例
JDBC API主要在java.sql和javax.sql两个包中,JDK9中他们合并到了java.sql模块中,采用模块化设计的话需要在模块的模块描述文件中加入requires java.sql。
JDBC编程的基本步骤是:
①、注册Driver操作对象。
②、取得Connection操作对象来进行SQL操作(通过Connection实例创建Statement / PreparedStatement实例,通过Statement来执行SQL语句,如果执行的是查询,则通过ResultSet读取结果集,如果是修改,则获得int结果)。
③、关闭Connection操作对象。
注册Driver操作对象:除了使用Class.forName()方法外,还有另外三种方式来完成注册Driver操作对象,可参考《Java JDK9学习笔记》。Class.forName()可能会抛出ClassNotFoundException异常。
取得Connection操作对象:DriverManager.getConnection()的参数为JDBC URL,对于SQLite来说,因为一般是直接在本地使用文件作为数据库,所以其JDBC URL为"jdbc:sqlite:demo.sqlite",其中的demo.sqlite为数据库文件名。对于My SQL来说JDBC URL
的格式为"jdbc::mysql://主机名:端口/数据库名?参数=值&参数=值",如"jdbc::mysql://localhost:3306/demo?user=root&password=123"。如果要使用中文存取的话必须指定参数useUnicode及characterEncoding来表明是否使用unicode及指定字符编码方式,如:"jdbc::mysql://localhost:3306/demo?user=root&password=123&useUnicode=true&characterEncoding=UTF8",如果要将URL写在XML配置中的话要使用"&"来替代"&",如:"jdbc::mysql://localhost:3306/demo?user=root&password=123&useUnicode=true&characterEncoding=UTF8"。DriverManager.getConnection()或者Connection的其它操作可能会抛出SQLException异常,SQLException还有个子类SQLWarning,在数据库执行时发生警示信息后会建立SQLWarning但不会抛出,可以使用Connection、Statement、ResultSet的getWarnings()来取得第一个SQLWarning,使用这个对象的getNextWarning()取得下一个警告,必要时可以将其抛出。如下为对于Connection异常的处理示例:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Main {
public static void main(String[] args)
{
Connection conn = null;
SQLException ex = null;
try{
conn = DriverManager.getConnection("jdbcUrl");
......
}catch (SQLException e){
ex = e;
}
finally {
if(conn != null){
try{
conn.close();
}catch (SQLException e){
if(ex == null){
ex = e;
}else{
ex.addSuppressed(e); //添加新的异常信息
}
}
}
if(ex != null) {
throw new RuntimeException(ex);
}
}
}
}
关闭Connection操作对象:可以调用Connection的isClosed()测试是否已经关闭与数据库的连接,close()来关闭连接,Connection、Statement、ResultSet等接口都是java.lang.AutoCloseable的子接口,可以使用尝试自动关闭资源语法:
try(Connection conn = DriverManager.getConnection("jdbcUrl")){
......
}catch(SQLException e){
throw new RuntimeException(e);
}
下面是一个完整的JDBC数据库编程的示例:
package xu;
import java.sql.*;
public class Test
{
public static void main (String[] args)throws Exception
{
//加载MySQL驱动(注册Driver操作对象)
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = null; //数据库连接对象
Statement st = null; //接口对象
ResultSet rs = null; //结果集对象
try
{
//通过JDBC驱动服务类DriverManager获得URL对应数据库的连接对象
DriverManager.setLoginTimeout(3); //设置登录超时时间而不是连接超时时间?
conn = DriverManager.getConnection("jdbc:mysql://192.168.0.38:3366/db1?useSSL=false&serverTimezone=Hongkong",
"leon", "123456");
//通过连接对象获得用于执行SQL语句的Statement接口对象
st = conn.createStatement();
//执行SQL语句,获得结果集对象ResultSet
//executeQuery(String sql)只能用于执行查询语句,它返回查询结果的ResultSet对象
rs = st.executeQuery("select * from orders");
//获得的ResultSet初始指向第一条记录之前
//ResultSet包含了一系列移动记录指针的方法,可以通过getXxx()或getObject()方法(参数传入列索引或列名)
//获得当前指针的列数据。
while(rs.next())
{
//获得记录里的数据
int id = rs.getInt(1); //通过字段位置获得字段的值,索引从1开始
int no = rs.getInt("No"); //通过字段的名称获得字段的值
String name = rs.getString(3);
}
//插入一条记录
//executeUpdate(String sql)/executeLargeUpdate(String sql)用于执行会改变数据库的更新语句(增删改),它返回受影响的行数。
int rows = st.executeUpdate("insert into person values(9, 1012, 'leon')");
System.out.println(rows);
//execute(String sql)可以执行任何SQL语句,返回true的话(执行的SQL语句为查询语句)就可以调用Statement的getResultSet()来获得查询语句的结果集,返回false(执行的SQL语句为更新语句)则可以调用Statement的getUpdateCount()获得更新语句影响的记录行数,比如增加了几条记录。
}
catch(Exception e)
{
System.out.println(e.getMessage());
e.printStackTrace();
}
finally
{
if(st != null)
st.close(); //Statement关闭时会自动关闭关联的ResultSet
if(conn != null)
conn.close();
}
}
}
2、预编译的Statement
PreparedStatement是Statement的子类,它是预编译的Statement对象,调用Connection的prepareStatement(String sql)获得,它可以避免数据库每次都编译SQL语句,以后每次只改变SQL命令的参数。对于需要反复执行相同结构的SQL语句使用PreparedStatement会更快,它比Statement多了设置SQL语句参数的setXxx(int idx, Xxx value)、setObject(int parameterIndex, Object x)方法。
使用PreparedStatement可以避免拼接SQL语句,比如如下的SQL就可以修改为使用PreparedStatement:
......
String sql = String.format("insert into messages values ('%s', '%s', '%s')",
strName, strEmail, strMsg);
statement.executeUpdate(sql);
......
使用PreparedStatement也可以防止SQL注入,比如一条SQL语句:String sql = "select * from userTable where userName='" + strUserName + "' and password='" + strPassword + "'"; 如果用户输入了用户名为"john",密码为"' or '1'='1",则整个sql语句为select * from userTable where userName=john and password='' or '1'='1',这相当于没有输入密码就查询了数据。使用PreparedStatement就可以防止该问题:
PreparedStatement stmt = conn.prepareStatement("select * from userTable where userName=? and password=?");
stmt.setString(1, strUserName);
stmt.setString(2, strPassword );
使用PreparedStatement的完整示例:
package xu;
import java.sql.*;
public class Test
{
public static void main (String[] args)throws Exception
{
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try
{
DriverManager.setLoginTimeout(3);
conn = DriverManager.getConnection("jdbc:mysql://192.168.0.38:3366/db1?useSSL=false&serverTimezone=Hongkong", "leon", "123456");
//获得预编译的Statement接口对象
st = conn.prepareStatement("select * from person where ID = ? and name = ?");
//设置SQL语句参数值
st.setInt(1, 1002);
st.setString(2, "abcd");
//execute(String sql)可以执行任何SQL语句,它返回boolean表示是否返回了ResultSet对象
boolean bRet = st.execute(); //执行SQL语句
if(bRet)
{
rs = st.getResultSet(); //获得查询到的结果集ResultSet对象
while(rs.next())
{
System.out.println(rs.getInt(1) + "\t" + rs.getString(2));
}
}
st = conn.prepareStatement("insert into person values(?, ?)");//插入一条记录
st.setInt(1, 1004);
st.setString(2, "test");
bRet = st.execute();
if(bRet)
{
int iCnt = st.getUpdateCount(); //获得DML语句影响的记录行数。
System.out.println(iCnt);
}
}
catch(Exception e)
{
System.out.println(e.getMessage());
e.printStackTrace();
}
finally
{
if(st != null)
st.close(); //会自动关闭ResultSet
if(conn != null)
conn.close();
}
}
}
3、存储过程
CallableStatement是用来调用存储过程的Statement对象,它是Statement的子类,通过Connection的prepareCall(String sql)获得CallableStatement接口对象,其使用与PreparedStatement类似,查询操作使用executeQuery()、更新操作使用executeUpdate():
//设置要执行的存储过程proc的参数
CallableStatement st = conn.prepareCall("call proc(?, ?, ?)");
st.setInt(1, 10); //传入参数
st.setInt(2, 90); //传入参数
st.registerOutParameter(3, Types.INTEGER); //传出参数
//执行存储过程
st.execute();
//获得执行结果:传出参数和存储过程中查询语句的结果
int iOut = st.getInt(3);
System.out.println(iOut);
rs = st.getResultSet();
while(rs.next())
{
System.out.println(rs.getInt(1) + "\t" + rs.getInt(2));
}
4、结果集
ResultSet是结果集对象,除了上面所说的next()和close()、getInt()、getString()方法外,还有以下常用方法:
absolute(int row):将记录指针移动到正数row行或倒数row行。
previous():将记录指针移动到上一行。
first():将记录指针移动到首行。
isFirst():是否是首行指针。
beforFirst():将记录指针移到首行之前。
last():将记录指针移动到末行。
afterLast():将记录指针移动到末行之后。
getRow():获得当前行号
在创建Statement或PreparedStatement时可以传入额外两个参数来设置结果集是可滚动的(默认是ResultSet.TYPE_FORWARD_ONLY只能向前,SQLite只支持这种模式,TYPE_SCROLL_SENSITIVE和TYPE_SCROLL_INSENSITIVE都是可前后滚动,前者对已经取出来的数据的修改敏感(包括update、delete、不含insert),后者不敏感)和可更新的(默认为只读)。可更新的结果集中数据必须来自一个表且必须包含主键列和选取所有NOT NULL的值,调用结果集的updateInt、updateString、updateArray等方法来更新当前记录的数据,最后调用updateRow来提交对本条记录的修改。
package xu;
import java.sql.*;
public class Jdbc {
public static void main (String[] args)throws Exception
{
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try
{
DriverManager.setLoginTimeout(3);
conn = DriverManager.getConnection("jdbc:mysql://192.168.0.38:3366/db1?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8", "leon", "123456");
//创建可滚动, 可更新的prepareStatement
st = conn.prepareStatement("select * from newtable2", ResultSet.TYPE_SCROLL_INSENSITIVE/*可滚动*/, ResultSet.CONCUR_UPDATABLE/*可修改*/);
rs = st.executeQuery();
rs.last();
int rowCnt = rs.getRow();
for(int i = rowCnt; i > 0; i--)
{
rs.absolute(i);
rs.updateString(2, "测试" + i); //修改记录指针所指记录的第二列的值
rs.updateRow(); //提交修改
}
}
catch(Exception e)
{
System.out.println(e.getMessage());
e.printStackTrace();
}
finally
{
if(st != null)
st.close();
if(conn != null)
conn.close();
}
}
}
下面是新增和删除数据的示例:
rs.moveToInsertRow();
rs.updateString(2, "mm");
rs.updateString(3, "gg");
rs.insertRow();
rs.moveToCurrentRow();
rs.absolute(iNum);
rs.deleteRow();
5、Blob、Clob对象
MySql中Blob是二进制长对象的意思,通常用来存储图片或声音等大文件,Clob用来存储大量的文字数据。可以通过PreparedStatement的setBlob()、setBinaryStream()、setBytes()来设定Blob数据,通过ResultSet的getBlob()、getBinaryStream()、getBytes()取得Blob数据,Blob的getBinaryStream()、getBytes()可以获得对应的InputStream和byte[]。
package xu;
import java.sql.*;
import java.io.*;
public class Test
{
public static void main (String[] args)throws Exception
{
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = null;
PreparedStatement stInsert = null;
PreparedStatement stQuery = null;
ResultSet rs = null;
try
{
DriverManager.setLoginTimeout(3);
conn = DriverManager.getConnection("jdbc:mysql://192.168.0.38:3366/db1?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8", "leon", "123456");
//保存Blob类型数据
stInsert = conn.prepareStatement("insert into imageTable values(?, ?)",
Statement.RETURN_GENERATED_KEYS); //Statement.RETURN_GENERATED_KEYS指定插入语句返回生成的主键
File file = new File("test.jpg");
InputStream is = new FileInputStream(file);
stInsert.setInt(1, 001);
stInsert.setBlob(2, is);
int affect = stInsert.executeUpdate();
System.out.println(affect + " record insert");
//获得Blob类型数据
stQuery = conn.prepareStatement("select img_data from imageTable where img_id = ?");
stQuery.setInt(1, 001);
rs = stQuery.executeQuery();
while(rs.next())
{
Blob blobImage = rs.getBlob(1);
if(blobImage.length() > 0)
{
FileOutputStream osFile = new FileOutputStream(new File("test.jpg"));
osFile.write(blobImage.getBytes(1, (int)blobImage.length()));
}
}
}
catch(Exception e)
{
System.out.println(e.getMessage());
e.printStackTrace();
}
finally
{
if(stInsert != null)
stInsert.close();
if(stQuery != null)
stQuery.close();
if(conn != null)
conn.close();
}
}
}