初识JDBC 如果我们要使用Java
来操作数据库,由于数据库的种类繁多,并且sql
语句并不完全相同,总而言之不同的数据库千差万别,这是不是意味着我们要对不同的数据库写不同的程序,这样的话对程序员的要求有点高,并且学习的成本也很大,所以我们要定义一套规范,要求对所有的数据库程序员只要写相同的程序就可以了。就相当于插座,我们定义好插座的标准,那么所有的商家必须按我的标准来,否则就不能使用插座。同理,在Java中定义好数据库的接口,定义接口中的方法有什么作用,而具体的实现细节则有数据库的厂商进行编写,程序员只要调用接口里面的方法就可以了。这里给出一个摘至维基百科的定义
Java Database Connectivity (JDBC) is an application programming interface (API) for the programming language Java, which defines how a client may access a database. It is a Java-based data access technology used for Java database connectivity.
所以使用JDBC
的好处便显而易见
程序员如果要开发访问数据库的程序,只需要会调用JDBC
接口中的方法即可,不用关注类是如何实现的
使用同一套Java
代码,进行少量的修改就可以访问其他JDBC
支持的数据库
我们使用Java
操作数据库,会使用到数据库的驱动,由各大数据库厂商提供,需要额外去下载,里面是对JDBC
接口实现的类,导入驱动jar包的具体的步骤如下
在项目中新建一个libs
文件夹
将jar
包复制到这个文件夹中
选中这个jar
包,右击找到"Add as Library"
上面是导入jar
包的通用步骤,后面会经常导入jar
包,所以需要熟记。
第一个JDBC程序 使用Java
操作数据库一般包括下面几步
注册驱动
获得连接数据库的对象
获得执行sql
语句的对象
执行sql
语句
释放资源(断开连接)
程序如下(具体类后面解释)
import java.sql.Connection;import java.sql.DriverManager;import java.sql.Statement;public class Demo01 { public static void main (String[] args) throws Exception { Class.forName("com.mysql.jdbc.Driver" ); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db1" , "root" , "root" ); String sql = "update account set balance = 2000 where id = 1" ; Statement statement = conn.createStatement(); statement.executeUpdate(sql); statement.close(); conn.close(); } }
核心类 下面就详细介绍JDBC
的核心类(接口)以及它们的功能
DriverManager
:驱动管理对象,功能如下
注册驱动:告诉程序该使用哪一个数据库驱动jar
包
mysql5
之后可以省略注册驱动的步骤,即不需要写Class.forName()
获取数据库连接
static Connection getConnection(String url, String user, String password)
url
:连接的路径
格式:jdbc:mysql://ip地址(域名):端口号/数据库名称?参数=参数值
例子:jdbc:mysql://localhost:3306/db1
如果ip
地址为本机地址且端口号为3306
,则上面可省略ip
地址和端口号,简写为:jdbc:mysql:///db1
如果出现乱码问题,可以指定参数?characterEncoding=utf8
user
:用户名
password
:密码
Connection
:数据库连接对象,相当于是在Java程序与数据库之间建立了一条通道,功能如下
获取执行sql
的对象
Statement createStatement()
PreparedStatement prepareStatement(String sql)
管理事务
开启事务:setAutoCommit(boolean autoCommit)
,传入false
即开启事务
提交事务:commit()
回滚事务:rollback()
Statement
:执行sql
的对象,含有如下方法
boolean execute(String sql)
:可以执行任意的sql
语句,不常用,了解即可
int executeUpdate(String sql)
执行DML(insert、update、delete)
语句、DDL(create,alter、drop)
语句
返回值:影响的行数,可以通过这个影响的行数判断DML
语句是否执行成功 返回值>0
的则执行成功,反之,则失败。
ResultSet executeQuery(String sql)
:执行DQL(select)
语句
ResultSet
:结果集对象,封装查询结果,数据库通过通道返回的结果
boolean next()
getXxx()
getXxx(int columnIndex)
getXxx(int columnLabel)
这里给出一个使用ResultSet
的例子,比如我们要去查询account
表
import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.Statement;public class Demo02 { public static void main (String[] args) throws Exception { Class.forName("com.mysql.jdbc.Driver" ); Connection conn = DriverManager.getConnection("jdbc:mysql:///db1" , "user" , "root" ); Statement statement = conn.createStatement(); String sql = "select * from account" ; ResultSet resultSet = statement.executeQuery(sql); while (resultSet.next()) { int id = resultSet.getInt("id" ); String name = resultSet.getString("name" ); double balance = resultSet.getDouble("balance" ); System.out.println(id + "---" + name + "---" + balance); } resultSet.close(); statement.close(); conn.close(); } }
JDBC工具类 其实上面的代码并不是十分的规范,因为资源的释放可能不能释放成功,因为前面发生异常就可以导致后面的程序执行不到,这样资源无法释放,就会导致内存越用越少(内存泄漏),所以我们应当将资源释放的代码写到finally
代码块中,如下
import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;import java.sql.Statement;public class Demo01 { public static void main (String[] args) { Connection conn = null ; Statement statement = null ; try { conn = DriverManager.getConnection("jdbc:mysql:///db1" , "root" , "root" ); statement = conn.createStatement(); String sql = "update account set balance = 2000 where id = 1" ; statement.executeUpdate(sql); } catch (SQLException e) { e.printStackTrace(); } finally { if (statement != null ) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null ) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
这时我们发现代码十分的冗长(特别是资源释放的部分),并且每次写代码都会重复这些,再者程序所连接的数据库是写死的,如果需要改变的话还需要改动程序,我们可以将这些信息写在一个jdbc.properties
的配置文件中,如果有改动直接改动配置文件即可
url=jdbc:mysql:///db1 user=root password=root driver=com.mysql.jdbc.Driver
将上面的这些操作抽离出来,写一个JDBC
的工具类,在工具类中读取配置文件,以及抽离出一些方法,这样不用每次都写这么长的代码
import java.io.FileNotFoundException;import java.io.FileReader;import java.io.IOException;import java.net.URL;import java.sql.*;import java.util.Properties;public class JDBCUtils { private static String url; private static String user; private static String password; private static String driver; static { try { Properties properties = new Properties(); ClassLoader classLoader = JDBCUtils.class.getClassLoader(); URL res = classLoader.getResource("jdbc.properties" ); String path = res.getPath(); properties.load(new FileReader(path)); url = properties.getProperty("url" ); user = properties.getProperty("user" ); password = properties.getProperty("password" ); driver = properties.getProperty("driver" ); Class.forName(driver); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection () throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close (Connection conn, Statement stam) { if (stam != null ) { try { stam.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null ) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } public static void close (Connection conn, Statement stam, ResultSet resultSet) { if (resultSet != null ) { try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } close(conn, stam); } }
用户登录练习 我们来写一个方法来判断用户输入的用户名和密码是否正确,思路就是根据用户输入的用户名和密码去数据库中查询,如果返回的ResultSet
有内容,那么就确定输入正确,否则失败。假设有下面这么一个表(user
)
+ | id | user | password | + | 1 | zhangsan | 123 | | 2 | lisi | 345 | +
下面是检查用户名和密码的方法
public static boolean login (String user, String password) { Connection conn = null ; Statement statement = null ; ResultSet resultSet = null ; try { conn = JDBCUtils.getConnection(); statement = conn.createStatement(); String sql = "select * from user where user = '" + user + "' and password = '" + password + "'" ; resultSet = statement.executeQuery(sql); return resultSet.next(); } catch (SQLException e) { e.printStackTrace(); } finally { JDBCUtils.close(conn, statement, resultSet); } return false ; }
下面在main
方法中写测试代码
public static void main (String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入用户名:" ); String user = scanner.nextLine(); System.out.println("请输入密码:" ); String password = scanner.nextLine(); boolean flag = login(user, password); if (flag) { System.out.println("登录成功" ); } else { System.out.println("用户名或密码错误" ); } }
我们来试验一下
请输入用户名: lisi 请输入密码: 234 用户名或密码错误
请输入用户名: zhangsan 请输入密码: 123 登录成功
PreparedStatement PreparedStatement
的作用同Statement
,也是用来执行sql
命令的,但是已经有了Statement
,为什么还需要PreparedStatement
呢? 那就要知道Statement
存在什么问题,我们来看上面的用户登录程序,如果我们登录时这么写
请输入用户名: daad 请输入密码: a' or ' a' = ' a 登录成功
得到的结果居然登录成功了,因为sql
语句被拼接成了
select * from user where user = 'daad' and password = 'a' or 'a' = 'a';
最后的or ‘a’ = ‘a’
得到的永远是true,所以总是可以得到返回结果,所以自然会显示登录成功,这种情况叫做SQL
注入。PreparedStatement
正是为了解决这一个问题的,上面出现问题是因为我们的sql
语句是拼接而成的,所以才会出现问题,PreparedStatement
采取的办法是首先使用?
占据位置(占位符),然后对?
所占据的位置进行赋值,赋值的方法为
setXxx()
:接收两个参数
第一个参数表示为第几个占位符赋值,从1
开始
第二个参数是值
所以我们将上面的登录代码修改如下
public static boolean login (String user, String password) { Connection conn = null ; PreparedStatement preparedStatement = null ; ResultSet resultSet = null ; try { conn = JDBCUtils.getConnection(); String sql = "select * from user where user = ? and password = ?" ; preparedStatement = conn.prepareStatement(sql); preparedStatement.setString(1 , user); preparedStatement.setString(2 , password); resultSet = preparedStatement.executeQuery(); return resultSet.next(); } catch (SQLException e) { e.printStackTrace(); } finally { JDBCUtils.close(conn, preparedStatement, resultSet); } return false ; }
这时还是使用与上面相同的操作
请输入用户名: dadd 请输入密码: a or 'a' = 'a' 用户名或密码错误
在实际中我们一般使用PreparedStatement
,因为它不仅可以防止SQL
注入,并且效率更高。
JDBC事务 事务就是一组sql
操作,这一组sql
操作要么同时成功,要么同时失败。比如转账,一方钱的支出与另一方钱的到账必须同时成功或者同时失败,没有这里钱转出去了,另一方却没有到账的情况。有关事务一般包含下面三个操作
开启事务
setAutoCommit(false)
开启事务后,直至提交,执行的sql
语句不会在数据库中生效,而是会写在日志中,只有提交后才会将根据日志修改数据库中的数据
默认是自动提交的,即每次执行一次sql
命令都会更改数据库中的内容,我们将自动提交关闭就相当于是开启了事务
提交事务
回滚事务
rollback()
事务执行失败,这时我们就要回到执行事务前的状态,这时会把日志中的内容清空
一般在catch
代码块中进行回滚操作
假设有下面这么一个表
+----+------+---------+ | id | NAME | balance | +----+------+---------+ | 1 | 张三 | 2000 | | 2 | 李四 | 1000 | +----+------+---------+
现在张三要给李四转500块钱
import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.SQLException;public class Demo04 { public static void main (String[] args) { double money = 500 ; Connection connection = null ; PreparedStatement preparedStatement1 = null ; PreparedStatement preparedStatement2 = null ; try { connection = JDBCUtils.getConnection(); String sql1 = "update account set balance = balance - ? where id = ?" ; String sql2 = "update account set balance = balance + ? where id = ?" ; preparedStatement1 = connection.prepareStatement(sql1); preparedStatement1.setDouble(1 , money); preparedStatement1.setInt(2 , 1 ); preparedStatement2 = connection.prepareStatement(sql2); preparedStatement2.setDouble(1 , money); preparedStatement2.setInt(2 , 2 ); preparedStatement1.executeUpdate(); preparedStatement2.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { JDBCUtils.close(connection, preparedStatement1); JDBCUtils.close(null , preparedStatement2); } } }
执行上面的程序然后再次查表得
+----+------+---------+ | id | NAME | balance | +----+------+---------+ | 1 | 张三 | 1500 | | 2 | 李四 | 1500 | +----+------+---------+
但是如果我们在执行张三支出500块后手动添加一个异常,这时李四则不会收到500块
preparedStatement1.executeUpdate(); int i = 3 / 0 ;preparedStatement2.executeUpdate();
+----+------+---------+ | id | NAME | balance | +----+------+---------+ | 1 | 张三 | 1000 | | 2 | 李四 | 1500 | +----+------+---------+
张三的钱减少了500块,但是李四却没有收到。这就是问题,所以我们要开启事务,并且在catch
代码块中进行回滚
import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.SQLException;public class Demo04 { public static void main (String[] args) { double money = 500 ; Connection connection = null ; PreparedStatement preparedStatement1 = null ; PreparedStatement preparedStatement2 = null ; try { connection = JDBCUtils.getConnection(); connection.setAutoCommit(false ); String sql1 = "update account set balance = balance - ? where id = ?" ; String sql2 = "update account set balance = balance + ? where id = ?" ; preparedStatement1 = connection.prepareStatement(sql1); preparedStatement1.setDouble(1 , money); preparedStatement1.setInt(2 , 1 ); preparedStatement2 = connection.prepareStatement(sql2); preparedStatement2.setDouble(1 , money); preparedStatement2.setInt(2 , 2 ); preparedStatement1.executeUpdate(); int i = 3 / 0 ; preparedStatement2.executeUpdate(); connection.commit(); } catch (Exception e) { try { if (connection != null ) { connection.rollback(); } } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } finally { JDBCUtils.close(connection, preparedStatement1); JDBCUtils.close(null , preparedStatement2); } } }
这次再次执行
+----+------+---------+ | id | NAME | balance | +----+------+---------+ | 1 | 张三 | 1000 | | 2 | 李四 | 1500 | +----+------+---------+
虽然还是发生了异常,但是张三和李四并没有发生上面一方转出一方没有到账的情况,而是转账失败。
数据库连接池 每次我们使用完连接对象后都会将该对象销毁,然后下次需要连接对象时又需要重新创建。如果考虑到有频繁的操作数据库的操作,每次创建销毁的开销是很大的,所以就有了数据库连接池,里面有很多的连接对象,当我们需要时就从里面拿,用完之后不是销毁,而是将对象归还给连接池,这样做能够有效的提升程序的性能。Java定义了一个DataSource
接口,我们可以通过该接口的getConnection()
方法获取一个连接,并且可以通过close()
方法归还这个连接(前提是这个连接是从连接池中获得的)。DataSource
接口的实现我们不需要关心,我们只要知道如何使用即可,我们学习如何使用两种数据库连接池技术
C3P0
Druid
:阿里巴巴提供,目前最好的数据库连接池之一
C3P0 C3P0
的使用步骤
导入jar
包
c3p0-0.9.5.2.jar
mchange-commons-java-0.2.12.jar
定义配置文件
只能为c3p0-config.xml
或c3p0.properties
,配置文件需放在src
目录下
使用实现类ComboPooledDataSource
获得连接池对象
下面给出配置文件c3p0-config.xml
的内容
<c3p0-config > <default-config > <property name ="driverClass" > com.mysql.jdbc.Driver</property > <property name ="jdbcUrl" > jdbc:mysql:///db1</property > <property name ="user" > root</property > <property name ="password" > root</property > <property name ="initialPoolSize" > 5</property > <property name ="maxPoolSize" > 10</property > <property name ="checkoutTimeout" > 3000</property > </default-config > </c3p0-config >
下面给出一个使用的示例
import com.mchange.v2.c3p0.ComboPooledDataSource;import javax.sql.DataSource;import java.sql.Connection;import java.sql.SQLException;public class Demo05 { public static void main (String[] args) throws SQLException { DataSource ds = new ComboPooledDataSource(); Connection conn = ds.getConnection(); } }
Druid Druid
的使用步骤
导入jar
包
定义配置文件
使用工厂方法DruidDataSourceFactory.createDataSource()
获得连接池
下面给出druid.properties
的内容
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql:///db1 username=root password=root initialSize=5 maxActive=10 maxWait=3000
想必上面的各参数的含义不必解释,那么就给出一个使用Druid
的例子
import com.alibaba.druid.pool.DruidDataSourceFactory;import javax.sql.DataSource;import java.io.InputStream;import java.sql.Connection;import java.util.Properties;public class Demo06 { public static void main (String[] args) throws Exception { Properties pro = new Properties(); InputStream is = Demo06.class.getClassLoader().getResourceAsStream("druid.properties" ); pro.load(is); DataSource ds = DruidDataSourceFactory.createDataSource(pro); Connection conn = ds.getConnection(); } }
工具类 我们发现每次使用连接池时会有很多重复的操作,同上面我们应该写一个工具类JDBCUtils
将一些操作抽离出来,JDBCUtils
类如下
import com.alibaba.druid.pool.DruidDataSourceFactory;import javax.sql.DataSource;import java.io.IOException;import java.sql.*;import java.util.Properties;public class JDBCUtils { private static DataSource ds; static { Properties pro = new Properties(); try { pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties" )); ds = DruidDataSourceFactory.createDataSource(pro); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection () throws SQLException { return ds.getConnection(); } public static void close (Connection conn, Statement stam) { if (stam != null ) { try { stam.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null ) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } public static void close (Connection conn, Statement stam, ResultSet resultSet) { if (resultSet != null ) { try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } close(conn, stam); } public static DataSource getDataSource () { return ds; } }
Spring JDBC Spring
对JDBC
进行了简单的封装,提供了一个JdbcTemplate
对象来简化JDBC
的开发,使用步骤为
导入jar
包
spring-beans-5.0.0.RELEASE.jar
spring-core-5.0.0.RELEASE.jar
spring-jdbc-5.0.0.RELEASE.jar
spring-tx-5.0.0.RELEASE.jar
commons-logging-1.2.jar
创建JdbcTemplate
对象
new JdbcTemplate(ds)
:ds
为数据库连接池对象
使用JdbcTemplate
对象的方法对数据库进行操作
update()
:执行DML
语句
第一个参数为sql
语句
第二个参数为可变参数,是占位符?
所对应的值
queryForMap()
:将结果封装为Map
对象
只能查询一行数据,将结果封装为Map
对象
将列名最为Key
,将字段值作为Value
queryForList()
:将结果封装为List
对象
查询多行数据,每一行数据封装为一个Map
对象,这些Map
对象会被添加到一个List
集合中返回
queryForObject()
:将结果封装为对象
query()
:将结果封装为JavaBean
对象
下面就来做一个简单的演示,假设db1
数据库中有一张表account
其中内容如下
+----+------+---------+ | id | NAME | balance | +----+------+---------+ | 1 | 张三 | 1000 | | 2 | 李四 | 1000 | +----+------+---------+
update 现在我们使用update
命令修改张三的balance
为2000
import org.junit.Test;import org.springframework.jdbc.core.JdbcTemplate;public class Demo07 { private JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource()); @Test public void testUpdate () { String sql = "update account set balance = 2000 where id = ?" ; template.update(sql, 1 ); } }
我们再次查询account
表
+----+------+---------+ | id | NAME | balance | +----+------+---------+ | 1 | 张三 | 2000 | | 2 | 李四 | 1000 | +----+------+---------+
查询张三所在行(id = 1)
,得到一个Map
对象,我们将其打印出来
@Test public void testQueryForMap () { String sql = "select * from account where id = ?" ; Map map = template.queryForMap(sql, 1 ); System.out.println(map); }
结果为
{id=1 , NAME=张三, balance=2000.0 }
queryForList 如果我们需要查询多条数据怎么办,这个时候就使用queryForList
,它会将每条数据封装为Map
集合,然后将这些Map
集合添加到List
集合中
@Test public void testQueryForList () { String sql = "select * from account" ; List<Map<String, Object>> mapList = template.queryForList(sql); for (Map<String, Object> map: mapList) { System.out.println(map); } }
得到的结果为
{id=1 , NAME=张三, balance=2000.0 } {id=2 , NAME=李四, balance=1000.0 }
query 更多的时候我们希望将得到的结果封装为一个对象,现在我们新建一个类Account
如下
public class Account { private Integer id; private String name; private Double balance; public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public String getName () { return name; } public void setName (String name) { this .name = name; } public Double getBalance () { return balance; } public void setBalance (Double balance) { this .balance = balance; } @Override public String toString () { return "Account{" + "id=" + id + ", name='" + name + '\'' + ", balance=" + balance + '}' ; } }
现在我们希望将获得结果封装为一个个Account
对象,这时我们就需要使用query()
方法了,query
方法的第一个参数是sql
语句,第二个参数是RowMapper<>
的实现类,这个类我们可以自己实现,也可以使用Spring
提供好的实现类,我们就使用Spring
提供好的实现类BeanPropertyRowMapper<>
,我们只要将Account
的class
属性传递进去即可
@Test public void testQuery () { String sql = "select * from account" ; List<Account> list = template.query(sql, new BeanPropertyRowMapper<Account>(Account.class)); for (Account account : list) { System.out.println(account); } }
结果为
Account{id=1 , name='张三' , balance=2000.0 } Account{id=2 , name='李四' , balance=1000.0 }
queryForObject 现在我们来查询account
表中右多少条数据,我们使用聚合函数count()
,得到的结果是一个long
类型的数字,我们使用queryForObject()
查询,接收两个参数,第一个参数是sql
命令,第二个参数是返回类型的class
属性
@Test public void testQueryForObject () { String sql = "select count(id) from account" ; Long num = template.queryForObject(sql, Long.class); System.out.println(num); }
结果为