JDBC
JDBC(Java Data Base Connectivity, java数据库连接)是一种用于执行SQL语句的javaAPI,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。
- 加载驱动
- 连接数据库
- 使用语句操作数据库
- 关闭数据库连接,释放资源
加载驱动(以MySQL举例)
Java连接MySQL需要驱动包,最新下载地址为:http://dev.mysql.com/downloads/connector/j/,解压后得到jar库文件,然后在对应的项目中导入该库文件。
MySQL8.0之前版本
驱动名:com.mysql.jdbc.Driver
加载方式:Class.forName("com.mysql.jdbc.Driver")
MySQL8.0之后版本
驱动名:com.mysql.cj.jdbc.Driver
加载方式:Class.forName("com.mysql.cj.jdbc.Driver")
连接关闭数据库
- DriverManager:驱动管理类,负责获取数据库的连接
- getConnection(String url, String user, String password):试图建立给定数据库的URL连接
- 数据库连接地址格式:jdbc:mysql://IP:端口号/数据库名称
- Connection接口:与特定的数据库连接(会话)
- createStatement():创建Statement执行数据库更删改查等操作
- close():立即释放此Connection对象的数据库和JDBC资源,而不是等待他们被自动释放
举个例子:
import java.sql.*;
public class MySQLDemo{
//MySQL8.0以下版本-JDBC数据库名及数据库URL,数据库名称为test_db
static final String JDBC_DRIVER = "com.mysql.Driver";
statis final String DB_URL="jdbc:mysql://localhost:3306/test_db"
//MySQL8.0以上版本-JDBC数据库名及数据库URL,数据库名称为test_db
//static final String JDBC_DRIVER="com.mysql.cj.jdbc.Driver";
//static final String DB_URL="jdbc:mysql://localhost:3306/test_db"
//数据库的用户名与密码,根据你自己的数据库进行设置
static final String User ="root";
static final String Password="123456";
public static void main(String... args){
Connection conn = null;
try{
//注册JDBC驱动
Class.forName(JDBC_DRIVER);
System.out.println("注册驱动成功");
//连接数据库并得到Connection会话
conn = DriverManager.getConnection(DB_URL, User, Password);
}catch(ClassNotFoundException e){
e.printStackTrace();
System.out.println("注册驱动失败")
}finally{
//最后关闭Connection会话,释放资源
if(conn !=null){
try{
conn.close();
}catch(SQLException e){
e.printStackTrace();
}
}
}
}
}
操作数据库(Statement)
- 作用:用于执行静态SQL语句并返回它所生成结果的对象;
- int executeUpdate(String sql)执行给定SQL语句,该语句可能为INSERT、UPDATE或DELETE语句,或者不返回任何内容的SQL语句(如SQL DDL 语句)
- void close():立即释放此Statement对象的数据库和资源,而不是等待该对象自动关闭
封装DBUtil工具类
public class DBUtil{
//MySQL8.0以下版本-JDBC数据库名及数据库URL,数据库名称为test_db
static final String JDBC_DRIVER = "com.mysql.Driver";
statis final String DB_URL="jdbc:mysql://localhost:3306/test_db"
//MySQL8.0以上版本-JDBC数据库名及数据库URL,数据库名称为test_db
//static final String JDBC_DRIVER="com.mysql.cj.jdbc.Driver";
//static final String DB_URL="jdbc:mysql://localhost:3306/test_db"
//数据库的用户名与密码,根据你自己的数据库进行设置
static final String User ="root";
static final String Password="123456";
/**
*获取数据库连接
*/
public Connection getConnection() throws Exception{
//注册JDBC驱动
Class.forName(JDBC_DRIVER);
//连接数据库并得到Connection会话
return DriverManager.getConnection(DB_URL, User, Password);
}
/**
*关闭连接
* @param conn
* @throws Exception
*/
public void close(Connection conn) throws Exception{
if(conn!=null){
conn.close();
}
}
/**
* 同时关闭Statement和Connection
*/
public void close(Statement stmt, Connection conn) throws Exception{
if(stmt !=null){
stmt.close();
}
if(conn !=null){
conn.close();
}
}
}
下面执行连接数据库均采用上述的DBUtil工具类。
创建测试数据
接下来我们在MySQL中创建test_db数据库并创建websites数据表,表结构如下:
CREATE TABLE `websites` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` char(20) NOT NULL DEFAULT '' COMMENT '站点名称',
`url` varchar(255) NOT NULL DEFAULT '',
`alexa` int(11) NOT NULL DEFAULT '0' COMMENT 'Alexa 排名',
`country` char(10) NOT NULL DEFAULT '' COMMENT '国家',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
测试插入:
public class TestInsert{
public static void main(String... args) throws Exception{
DBUtil dbUtil = new DBUtil();
//SQL语句,这里的1也可以设置为null,因为为它设置了自增
String sql="INSERT INTO `websites` VALUES ('1', 'Google', 'https://www.google.cm/', '1', 'USA')";
Connection conn = dbUtil.getConnection();//获取数据库连接
Statement stmt = conn.createStatement();//获取Statement
//进行SQL语句进行插入,其中返回结果表示数据库操作的数量,这里插入一条数据返回的就是1,插入N条返回N。
int result = stmt.executeUpdate(sql);
System.out.println("操作的结果:"+ result+"数据");
stmt.close();关闭stmt
dbUtil.close(conn);//关闭Connection
}
}
这里我们将需要插入的数据直接封装在SQL语句中,但是实际开发的时候是由外部传入的,我们可以进行修改为:
public class TestInsert{
public static int addWebsite(String name, String url, int alexa, String country) throws Exception{
DBUtil dbUtil = new DBUtil();
//SQL语句,这里的1也可以设置为null,因为为它设置了自增
String sql="INSERT INTO `websites` VALUES (null, '"+name+"', '"+website+"', '"+alexa+"', '"+country+"')";
Connection conn = dbUtil.getConnection();//获取数据库连接
Statement stmt = conn.createStatement();//获取Statement
//进行SQL语句进行插入,其中返回结果表示数据库操作的数量,这里插入一条数据返回的就是1,插入N条返回N。
int result = stmt.executeUpdate(sql);
System.out.println("操作的结果:"+ result+"数据");
stmt.close();关闭stmt
dbUtil.close(conn);//关闭Connection
return result;
}
public static void main(String... args) throws Exception{
int result=addWebsite("Google", "https://www.google.com/", 1, "USA");
if(result==1){
System.out.println("添加成功");
}else{
System.out.println("添加失败");
}
}
}
但是传入那么多参数并不符合面向对象的思想,我们应该将网站信息封装成Java类,以对象的形式进行操作,具体如下:
class Website{
private int id;
private String name;
private String url;
private int alexa;
private String country;
//下面是对应的构造方法、Getter和Setter方法,请自行添加
}
添加完Website类之后,TestInsert添加AddWebsite2方法,操作如下:
public static int addWebsite2(Website website) throws Exception{
DBUtil dbUtil = new DBUtil();
//SQL语句,这里的1也可以设置为null,因为为它设置了自增
String sql="INSERT INTO 'websites' VALUES (null, '"+website.getName()+"', '"+website.getUrl()+"', '"+website.getAlexa()+"', '"+website.getCountry()+"')";
Connection conn = dbUtil.getConnection();//获取数据库连接
Statement stmt = conn.createStatement();//获取Statement
//进行SQL语句进行插入,其中返回结果表示数据库操作的数量,这里插入一条数据返回的就是1,插入N条返回N。
int result = stmt.executeUpdate(sql);
System.out.println("操作的结果:"+ result+"数据");
stmt.close();关闭stmt
dbUtil.close(conn);//关闭Connection
return result;
}
修改main方法如下:
public static void main(String... args) throws Exception{
Website website = new Website("Google", "http://www.google.com/", 1, "USA");
int result=addWebsite2(website);
System.out.println(result==1?"添加成功":"添加失败");
}
其中更新、删除操作进本相同,不同的是数据库语句,这里就不过多的举例子!
PreparedStatement实现增删改
PreparedStatement是Statement的子接口,属于预处理操作。与Statement不同的是,PreparedStatement在操作时,是先在数据表中准备好一条SQL语句,但是此SQL语句的具体内容暂时不设置,而是在之后进行设置。
插入
我们仍然使用上面的数据库,使用PreparedStatement插入一个网站,具体如下:
DBUtil dbUtil = new DBUtil();
public static int addWebsite(Website website) throws Exception{
Connection conn = dbUtil.getConnection();
String sql = "INSERT INTO websites values(null, ?, ?, ?, ?)";
PreparedStatement pstmt = conn.preparedStatement(sql);
pstmt.setString(1, website.getName());//从1开始
pstmt.setString(2, website.getUrl());
pstmt.setInt(3, website.getAlexa());
pstmt.setString(4, website.getCountry());
//所以SQL语句的四个问号分别对应name、URL、alexa、country
int result = conn.executeUpdate();
dbUtil.close(pstmt, conn);
return result;
}
更新
现在数据库中已经存在了(id=1,name=Google,url="https://www.google.com/",alexa=1,country=USA)的数据了。如果我们要将更改这条数据,用PreparedStatement接口应该怎么做呢?
DBUtil dbUtil = new DBUtil();
public static int updateWebsite(Website website) throws Exception{
Connection conn = dbUtil.getConnection();
String sql ="UPDATE websites set name=?,url=?,alexa=?,country=? where id=?";
PreparedStatement pstmt = conn.preparedStatement(sql);
pstmt.putString(1, website.getName());
pstmt.putString(2, website.getUrl());
pstmt.putInt(3, website.getAlexa());
pstmt.putString(4, website.getCountry());
pstmt.putInt(5, website.getId());
int result =pstmt.executeUpdate();
dbUtil.close(pstmt, conn);
return result;
}
删除
假设我们要删除id=1的数据,代码如下:
DBUtil dbUtil = new DBUtil();
//删除指定id的websiste
public static int deleteWebsite(int id) throws Exception{
Connection conn = dbUtil.getConnection();
String deleteSql ="DELETE from websites where id=?";
PreparedStatement pstmt = conn.preparedStatement(deleteSql);
pstmt.putInt(1, id);
int result=pstmt.executeUpdate();
dbUtil.close(pstmt, conn);
return result;
}
ResultSet结果集
当我们查询数据库时,返回的是一个二维的结果集,我们需要使用ResultSet来遍历结果,获取每一行的数据。
ResultSet光标最初位于第一行之前,第一次调用next()方法使第一行成为当前行,第二次调用使第二行成为当前行,以此类推。
- boolean next():将光标从当期位置向前移一行;
- String getString(int columnIndex):以Java编程语言中String的形式获取ResultSet对象的当前行中指定列的值;
- String getString(int columnLabel):以Java编程语言中String的形式获取ResultSet对象的当前行中指定列的值。
实战
DBUtil dbUtil = new DBUtil();
public List<Website> queryAllWebsite()throws Exception{
Connection conn = dbUtil.getConnection();
String querySql = "SELECT * from websites";
PreparedStatement pstmt = conn.preparedStatement(querySql);
ResultSet rs = pstmt.executeQuery();//executeQuery执行查询,返回ResultSet结果集
List<Website> websites = new ArrayList();
while(rs.next()){
int id = rs.getInt(1);//获取第一列的值:id
String name=rs.getString(2);//获取第二列的值:name
String url=rs.getString(3);//获取第三列的值:url
int alexa=rs.getInt(4);//获取第四列的值:alexa
String country=rs.getString(5);//获取第五列的值:country
Website website=new Website(id, name, url, alexa, country);
websites.add(website);
}
dbUtil.close(pstmt, conn);
return websites;
}
在while语句中我们也可以直接使用字段名称进行查询,操作如下:
int id = rs.getInt("id");
String name=rs.getString("name");
String url=rs.getString("url");
int alexa=rs.getInt("alexa");
String country=rs.getString("country");
处理大数据对象
- CLOB:(character large object)可以存储字符大数据对象,比如长篇小说;
- BLOB:(binary large object)可以存放二进制大数据对象,比如图片、电影、音乐;
CLOB实战
因为CLOB是存储字符大数据对象,我们可以设置字段的类型为TEXT或者LONGTEXT
- 在websites表中增加
about
字段,字段类型为longtext - 假设在电脑的C盘根目录下新增一个helloworld.txt文本文件,内容不限;
- 在Website类中增加类型为File的about字段。
实例代码如下:
public class TestClob{
DBUtil dbUtil = new DBUtil();
//插入网站
public int addWebsite(Website website) throws Exception{
Connection conn = dbUtil.getConnection();
String sql = "INSERT INTO websites values(null, ?,?,?,?,?)";//多了about的?
PreparedStatement pstmt = conn.preparedStatement(sql);
pstmt.putString(1, website.getName());
pstmt.putString(2, website.getUrl());
pstmt.putInt(3, website.getAlexa());
pstmt.putString(4, website.getCountry());
File about=website.getAbout();
InputStream in = new FileInputStream(about);
pstmt.setAsciiStream(5, in, in.length());//这里不是putString而是setAsciiStream
int result=pstmt.excuteUpdate();
dbUtil.close(pstmt, conn);
return result;
}
//读取网站
public void queryWebsite(int id)throws Exception{
Connection conn = dbUtil.getConnection();
String sql="SELECT * from websites where id=?";
PreparedStatement pstmt = conn.preparesStatement(sql);
pstmt.putString(1, id);
ResultSet rs = pstmt.excuteQuery();
while(rs.next()){
int id = rs.getInt("id");//依次获取name|country|alexa|url即可
Clob clob = rs.getClob("about");//先通过getClob获取Clob对象
String about=clob.getSubString(1,(int)clob.length());//利用getSubString获取内容
//也可以通过getAsciiStream流的形式获取内容
System.out.println("id:"+id+"\tabout:" + about);
}
dbUtil.close(pstmt, conn);
}
public static void main(String... args) throws Exception{
Website website = new Website("Google","http://www.google.com/", 1, "USA");
File fAbout = new File("C:/helloworld.txt");
website.setAbout(fAbout);//设置文件
int result = addWebsite(website);
System.out.println(result==1?"添加成功":"添加失败");
}
}
BLOB实战
因为Blob是存储二级制文件,包括图片、音乐等,数据库中可以申明为BLOB、LONGBLOB类型。
- 在websites表中增加logo字段(网站logo),字段类型为BLOB;
- 假设在电脑的C盘根目录下存放网站logo的图片名为logo.jpg;
- 在Website类中增加类型为File的logo字段
实例代码如下:
public class TestClob{
DBUtil dbUtil = new DBUtil();
//插入网站
public int addWebsite(Website website) throws Exception{
Connection conn = dbUtil.getConnection();
String sql = "INSERT INTO websites values(null, ?,?,?,?,?,?)";//多了一个logo的?
PreparedStatement pstmt = conn.preparedStatement(sql);
pstmt.putString(1, website.getName());
pstmt.putString(2, website.getUrl());
pstmt.putInt(3, website.getAlexa());
pstmt.putString(4, website.getCountry());
File about=website.getAbout();
InputStream in = new FileInputStream(about);
pstmt.setAsciiStream(5, in, in.length());//这里不是putString而是setAsciiStream
File logo = website.getLogo();
InputStream logoIn= new FileInputStream(logo);
pstmt.setBinaryStream(6, logoIn, logoIn.length());//这里是setBinaryStream
int result=pstmt.excuteUpdate();
dbUtil.close(pstmt, conn);
return result;
}
//读取网站
public void queryWebsite(int id)throws Exception{
Connection conn = dbUtil.getConnection();
String sql="SELECT * from websites where id=?";
PreparedStatement pstmt = conn.preparesStatement(sql);
pstmt.putString(1, id);
ResultSet rs = pstmt.excuteQuery();
while(rs.next()){
int id = rs.getInt("id");//依次获取name|country|alexa|url即可
//获取about
Clob clob = rs.getClob("about");//先通过getClob获取Clob对象
String about=clob.getSubString(1,(int)clob.length());//利用getSubString获取内容
//获取网站logo
Blob bLogo = rs.getBlob("logo");
//定义文件输出流,将文件保存到D盘根目录
FileOutputStream out=new FileOutputStream(new File("D:/logo.jpg"));
out.write(bLogo.getBytes(1, ((int)bLogo).length()));
out.close();
System.out.println("id:"+id+"\tabout:" + about);
}
dbUtil.close(pstmt, conn);
}
public static void main(String... args) throws Exception{
Website website = new Website("Google","http://www.google.com/", 1, "USA");
File fAbout = new File("C:/helloworld.txt");
website.setAbout(fAbout);//设置文件
File fLogo = new File("C:/logo.jpg");
website.setLogo(fLogo);
int result = addWebsite(website);
System.out.println(result==1?"添加成功":"添加失败");
}
}
CallableStatement调用存储过程
CallableStatement主要是调用数据库中的存储过程,是PreparedStatement的子接口。使用CallableStatement可以接收存储过程的返回值。
- void registerOutParameter(int parameterIndex, int sqlType):按顺序为止parameterIndex将OUT参数注册为JDBC类型sqlType
首先添加存储过程,根据website的id获取website的名称,操作如下:
//创建存储过程
DELIMITER &&
CREATE PROCEDURE pro_getSiteNameById(IN siteId INT, OUT sn CHAR(20))
BEGIN
select name into sn from websites where id=siteId;
END
&&
DELIMITER ;
//测试存储过程
CALL pro_getSiteNameById(1, @siteName);//执行
SELECT @siteName;//输出
CallableStatement调用存储过程,代码如下:
public class TestProcedure{
DBUtil dbUtil = new DBUtil();
public String getSiteNameById(int siteId) throws Exception{
Connection conn = dbUtil.getConnection();
String sql="{CALL pro_getSiteNameById(?,?)}";
CallableStatement cstmt = conn.prepareCall(sql);//prepareCall得到CallableStatement
cstmt.setInt(1, siteId);//设置第一个参数
cstmt.registerOutParameter(2, Types.CHAR);//注册第二个参数类型
cstmt.execute();
String siteName = cstmt.getString("sn");
dbUtil.close(cstmt,conn);
return siteName;
}
}
使用元数据分析数据库
- DatabaseMetaData:获取数据库基本信息,包括版本、名称以及表信息等
- String getDatabaseProductName():数据库产品的名称
- int getDriverMajorVersion():获取JDBC驱动的主版本号
- int getDriverMinorVersion():获取JDBC驱动的次版本号
- String getDriverName:获取驱动名称
- ResultSetMetaData:获取ResultSet对象中列的基本信息
- int getColumnCount():返回此ResultSet对象中的列数
- String getColumnName(int column):获取指定列的名称
- int getColumnTypeName(int column):获取指定列的SQL类型名称
实例代码如下:
public void aboutDatabaseMetaData() throws Exception{
DBUtil dbUtil = new DBUtil();
Connection conn = dbUtil.getConnection();
DatabaseMetaData metaData = conn.getMetaData();//获取元数据
System.out.println("数据库名称:"+ metaData.getDatabaseProductName());
System.out.println("数据库主版本号:" + metaData.getDriverMajorVersion());
}
public void aboutResultMetaData() throws Exception{
DBUtil dbUtil = new DBUtil();
Connection conn = dbUtil.getConnection();
String sql ="select * from websites";
PreparedStatement pstmt = conn.preparedStatement();
ResultSet rs = pstmt.executeQuery(sql);
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
System.out.println("website 共有:" + columnCount+" 列");
for(int i=0; i<columnCount; i++)
{
System.out.println("第"+i+"列名称为:" + metaData.getColumnName(i)
+"\t 列的类型为:" + metaData.getColumnTypeName(i));
}
}
JDBC处理事务
所谓事务就是所有的操作要么一起成功,要么一起失败。事务本身具备:
- 原子性:是不可再分割的最小单元,相当于一个个小的数据库操作,这些操作必须同时成功,如果一个失败则一切操作将全部失败;
- 一致性:数据库操作前后是一致的,保证数据有效性。如果事务正常操作则系统维持有效性,如果出现了错误,则回到最原始状态,也要维持有效性;
- 隔离性:多个事务可以同时进行且彼此之间无法访问,只有当事务完成最终操作时,才可以看到结果;
- 持久性:事务完成后,它对系统的影响是持久的。该修改即使出现致命的系统故障也将一直保持。
创建账户表
首先创建账户表account,包含id、账户名称和账户余额,代码如下:
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` char(20) NOT NULL DEFAULT '' COMMENT '站点名称',
`accountBalance` INTEGER NOT NULL DEFAULT '0' COMMENT '账户余额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
然后插入两个账户,分别为(张三,1000)和(李四,1000)。当张三向李四转账500的时候,其实出发两个数据库操作:
- 张三账户的余额变成500
- 李四账户的月变成1500
必须保证上述两个操作同时成功,才能认为转账操作是成功,如果有一个失败则必须回到转账操作之前的状态(两个人都是1000)。
不用事务(存在风险)
public class Test{
DBUtil dbUtil = new DBUtil();
/**
* 账户转出
*/
public void transferOut(Connection conn, String accountName, int money) throws Exception{
String sql = "UPDATE account set accountBalance=accountBalance-? where name=?";
PreparedStatement pstmt = con.preparedStatement(sql);
pstmt.setInt(1, 500);
pstmt.setString(2, accountName);
pstmt.executUpdate();
}
/**
* 账户转入
*/
public void transferIn(Connection conn, String accountName, int money) throws Exception{
String sql = "UPDATE account set accountBalance=accountBalance+? where name=?";
PreparedStatement pstmt = con.preparedStatement(sql);
pstmt.setInt(1, 500);
pstmt.setString(2, accountName);
pstmt.executUpdate();
}
public static void main(String... args) throws Exception{
Connection conn = dbUtil.getConnection();
int money=500;
transferOut(conn, "张三", money);
transferIn(conn, "李四", money);
dbUtil.close(conn);
}
}
以上操作是存在风险的并不能保证两个操作能够同时成功。
采用事务操作
转入和转出操作与上面是一致的,主要是修改main方法,代码如下:
public static void main(String... args){
Connection conn;
try{
conn = dbUtil.getConnection();
conn.setAutoCommit(false);//首先将自动提交进行关闭
System.out.println("张三开始向李四转账");
int money=500;
transferOut(conn, "张三", money);
transferIn(conn, "李四", money);
System.out.println("转账成功");
}catch(Exception e){
if(conn !=null){
try{
conn.rollback(); //当操作失败的时候回滚到之前的状态
}catch(Exception e){
e.printStackTrace();
}
}
}finally{
if(conn !=null){
try{
conn.commit();//提交事务
conn.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
setAutoCommit、rollback、commit三个方法的结合实现了事务的操作!
保存点
假设存在这种情况,张三将钱转给第三方,由第三方转给李四,也就是张三负责转,至于李四是否收到并不关心。
那么我们可以使用保存点来实现这个功能,具体操作如下:
public static void main(String... args){
Connection conn;
SavePoint sp;
try{
conn = dbUtil.getConnection();
conn.setAutoCommit(false);//首先将自动提交进行关闭
System.out.println("张三开始向李四转账");
int money=500;
transferOut(conn, "张三", money);
sp = conn.setSavePoint();//设置一个保存点
transferIn(conn, "李四", money);
System.out.println("转账成功");
}catch(Exception e){
if(conn !=null){
try{
//当操作失败的时候回滚到sp这个保存点的位置
conn.rollback(sp);
}catch(Exception e){
e.printStackTrace();
}
}
}finally{
if(conn !=null){
try{
conn.commit();//提交事务
conn.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}