本文已参与「新人创作礼」活动,一起开启掘金创作之路。
概述
Java中的数据存储技术
编辑
JDBC介绍
编辑
编辑
编辑
编辑
JDBC提供了接口也就是规范,具体实现细节由具体公司提供驱动。
为什么要定义规范,如果没有规范,就添加数据命令而言,Mysql可能定义为add,而Oracle定义为insert,SQLServer定义为put,还有可能每个方法传入的参数个数不同,操作的步骤数不同,就导致用Java操作不同数据库时,代码通用性低。
SUN公司提供这套规范,让各个公司去实现实现类,这些实现类的集合就是JDBC驱动。
JDBC体系结构
编辑
JDBC编写步骤
编辑
Java.sql包在jdk中有。
驱动包手动需要导入,根据不同数据库系统导入不同的包。
创建Connection。就像操作数据库客户端时也要先连接数据库,这里不用客户端操作数据库也要连接。
编辑
Statement去帮助执行SQL。有查询和增删改。
ResultSet,当执行的是查询就需要有结果集对象了。
ODBC是微软提供的操作不同数据库的API。先连接JDBC再连接ODBC再注册驱动的一种方式。
URL
编辑
URL就是定位到某一个数据库。
在网上定位一个具体资源时的URL格式:http://localhost:8080/具体资源。
这里jdbc:mysql://就相当于http://。
获取数据库连接
因为JDBC是用来定义规范的,所以JDBC的API中大部分都是接口。
获取连接的四种方式
方式1:
public class JDBC_Demo1 { public static void main(String[] args) throws SQLException { //获取驱动,也就是获取数据库系统厂商提供的jar包。 Driver driver = new com.mysql.cj.jdbc.Driver(); //定位到数据库 String url = "jdbc:mysql://localhost:3306/demo"; //将用户名和密码封装在Properties中 Properties info = new Properties(); info.setProperty("user","root"); info.setProperty("password","niit"); Connection connection = driver.connect(url,info); System.out.println(connection); } /* 你要去西藏旅游 Driver是车 url是目的地 info是钥匙 */ }
方式2:
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException { //方式2是对方式1的迭代 //为了让程序有更好的可移植性,要减少第三方程序的出现, //比如new com.mysql.cj.jdbc.Driver()就是第三方 //1.获取Driver实现类对象,使用反射 //反射机制指的是程序在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法 Class clazz = Class.forName("com.mysql.cj.jdbc.Driver"); Driver driver = (Driver) clazz.newInstance(); //2.提供要连接的数据库 String url = "jdbc:mysql://localhost:3306/demo"; //3.提供连接需要的用户名和密码 Properties info = new Properties(); info.setProperty("user","root"); info.setProperty("password","niit"); //4.获取连接 Connection connection = driver.connect(url,info); System.out.println(connection); } }
方式3:
public class JDBC_Demo3 { //方式3:使用DriverManager替换Driver public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException { //获取Deriver实现类对象 Class clazz = Class.forName("com.mysql.cj.jdbc.Driver"); Driver driver = (Driver) clazz.newInstance(); //注册驱动 DriverManager.registerDriver(driver); String url = "jdbc:mysql://localhost:3306/demo"; String user = "root"; String password = "niit"; //获取连接 DriverManager.getConnection(url,user,password); } }
方式4:可以只是加载驱动,不用显式的注册驱动了
public class JDBC_Demo4 { @Test public void testConnection() throws ClassNotFoundException, SQLException { //方式3的优化 //1.三个基本信息 String url = "jdbc:mysql://localhost:3306/demo"; String user = "root"; String password = "niit"; //2.获取Driver实现类对象 //注册驱动这个活Driver中静态代码块做了,静态代码块随着类的加载而执行, Class.forName("com.mysql.cj.jdbc.Driver"); //3.获取连接 Connection connection = DriverManager.getConnection(url,user,password); System.out.println(connection); } }
为什么可以省略注册驱动?
利用反射获取Driver对象时,随着类的加载执行了静态代码块中的代码。
编辑
自动注册了驱动,注册驱动理解为具体和哪家数据库提供的类文件对接。
注册了驱动就等于准备好了操作数据库的工具,就可以连接数据库进行操作了。
方式5:直接连接
public class JDBC_Demo4 { @Test public void testConnection() throws ClassNotFoundException, SQLException { //方式3的优化 //1.三个基本信息 String url = "jdbc:mysql://localhost:3306/demo"; String user = "root"; String password = "niit"; //2.获取Driver实现类对象 //注册驱动这个活Driver中静态代码块做了,静态代码块随着类的加载而执行, //Class.forName("com.mysql.cj.jdbc.Driver"); //3.获取连接 Connection connection = DriverManager.getConnection(url,user,password); System.out.println(connection); } }
连接时,自动获取对象,并且注册驱动。
但不推荐这样,这样适用于MySQL,但不一定适用于别的数据库。
编辑
方式6:最终版,基本信息暴露在代码中不安全。
public class JDBC_Demo5 { public static void main(String[] args) throws ClassNotFoundException, IOException, SQLException { //properties读取路径默认是从当前module的src下 InputStream is = JDBC_Demo5.class.getClassLoader().getResourceAsStream("connection\jdbc.properties"); Properties properties = new Properties(); properties.load(is); String user = properties.getProperty("user"); String password = properties.getProperty("password"); String url = properties.getProperty("url"); String driverClass = properties.getProperty("driverClass"); Class.forName(driverClass); Connection connection = DriverManager.getConnection(url,user,password); System.out.println(connection); } }
将数据库连接需要的基本信息放入配置文件中,通过读取配置文件的方式获取连接。
好处:
1.实现了数据与代码的分离,实现了解耦。
解耦的好处比如想要改这些数据可以在配置文件中修改,数据只在配置文件中,则代码和数据分开后,各自的可读性都提高。如果数据嵌套在代码中,如果一个不了解这些代码的人来修改数据就会比较麻烦,而让他直接去修改配置文件就会比较简单。
2.后期项目需要配置到服务器时需要打包,如果需要修改配置文件信息,可以避免程序重新打包。如果数据在程序中如果要更改数据就要对这个Java文件重新打包。
封装连接和关闭资源
public class JDBCUtils { //静态方法不需要获取对象 public static Connection getConnection(){ try{ //加载配置文件 InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties"); Properties properties = new Properties(); properties.load(is); //获取基本信息 String url = properties.getProperty("url"); String user = properties.getProperty("user"); String password = properties.getProperty("password"); String driverClass = properties.getProperty("driverClass"); //与数据库连接 Class.forName(driverClass); Connection connection = DriverManager.getConnection(url, user, password); return connection; }catch (Exception e){ e.printStackTrace(); } return null; } //关闭资源 public static void closeResource(Connection connection, Statement ps,ResultSet resultSet){ try{ if(connection!=null) connection.close(); if(ps!=null) ps.close(); if(resultSet!=null) resultSet.close(); }catch (Exception e){ e.printStackTrace(); } } }
user=root password=niit url=jdbc:mysql://localhost:3306/demo driverClass=com.mysql.cj.jdbc.Driver
使用PreparedStatement实现CRUD操作
CRUD:Create,Retrieve,Update,Delete 增删改查
介绍
操作和访问数据库
编辑
Statement操作数据表的弊端
需要拼写SQL语句,并且有SQL注入的风险。
补充:用scanner输入时,用sc.next()输入时遇到空格或换行结束,用sc.nextLine时只以换行为结束。
使用PreparedStatement
PreparedStatement是Statement的子接口。
一般的PreparedStatement操作
public class TestPreparedStatement { public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException { //加载配置文件 InputStream is = TestPreparedStatement.class.getClassLoader().getResourceAsStream("jdbc.properties"); Properties properties = new Properties(); properties.load(is); //获取基本信息 String url = properties.getProperty("url"); String user = properties.getProperty("user"); String password = properties.getProperty("password"); String driverClass = properties.getProperty("driverClass"); //与数据库连接 Class.forName(driverClass); Connection connection = DriverManager.getConnection(url,user,password); //操作数据库 //预编译sql语句,返回PreparedStatement对象 String sql = "insert into demo(userName,age) values(?,?)";//?是占位符 PreparedStatement preparedStatement = connection.prepareStatement(sql); //填充占位符 preparedStatement.setString(1,"小猪"); preparedStatement.setInt(2,18);//日期型用setDate //执行sql命令 preparedStatement.execute(); //资源的关闭 //为了避免空指针异常 if(preparedStatement!=null) preparedStatement.close(); if (connection!=null) connection.close(); } //增删改 //向demo表中修改数据 }
通用的增删改操作
public class Test2 { public static void main(String[] args) throws SQLException, IOException, ClassNotFoundException { //建议表名上加一个`,着重符,在tab键上面,假如表面是关键字,就会报错,加上`就可以避免 String sql = "delete from `demo` where age = ?"; update(sql,18); } //通用增删改操作 public static void update(String sql,Object ...args){//可变形参 try{ //获取连接 Connection connection = JDBCUtils.getConnection(); //预编译sql PreparedStatement preparedStatement = connection.prepareStatement(sql); //填充占位符 //遍历可变数组,数组的内容就是传进来填写占位符的数据 for (int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行 preparedStatement.execute(); //关闭资源 JDBCUtils.closeResource(connection,preparedStatement,null); }catch (Exception e){ e.printStackTrace(); } } }
增删改的操作不需要返回结果,只需要将占位符填写就可以。所以不同的增删改操作只是填写的占位符的个数和值不同。
一般的查询操作
//查询 public class Test3 { @Test public void testQuery() throws Exception{ Connection connection = JDBCUtils.getConnection(); String sql = "select * from demo where age = ?"; PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1,21); //执行并返回结果集 ResultSet resultSet = preparedStatement.executeQuery(); //处理结果集 //next功能:判断下面是否有元素,有的话下移,没有的话不下移,说明有两个功能,第一个是判断,第二个是移动 if(resultSet.next()){ //获取当前一行数据的各个字段值 String name = resultSet.getString(1); int age = resultSet.getInt(2); //将数据封装为一个对象输出查询结果 System.out.println(new User(name, age)); } JDBCUtils.closeResource(connection,preparedStatement,resultSet); } }
next的作用:1.判断下面是否有数据返回true或false。 2.如果有数据就下移,如果没数据就不下移。
这里用到了ORM思想
编辑
编辑
Java属性类型与MySQL字段类型对应
编辑
日期型如何传值。
编辑
针对于一个表的通用查询操作
//针对某个表的通用的查询,针对于查询时需要查询的字段个数不同的通用 //不同的表需要的JavaBean属性不同,所以是针对同一个表的通用查询 //select 字段1,字段2... from table where 字段 = ? public class Test4 { @Test public void test(){ //JavaBean的属性名和字段名要相同,因为要根据属性名要给该域赋值,这个属性名是从结果集的列名上获取 String sql = "select userName,age from demo where age = ?"; System.out.println(testQuery(sql, 21)); } public User testQuery(String sql,Object ...args){ Connection connection = null; PreparedStatement preparedStatement = null; ResultSet resultSet = null; try{ //获取连接 connection = JDBCUtils.getConnection(); preparedStatement = connection.prepareStatement(sql); //填写占位符 for(int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行并返回结果集 resultSet = preparedStatement.executeQuery(); //对结果集的一条结果进行封装 //获取结果集的列数,从而决定取的时候取多少次 //获取结果集的元数据,修饰现有数据的数据 ResultSetMetaData metaData = resultSet.getMetaData(); //获取列数 int columnCount = metaData.getColumnCount(); if(resultSet.next()){ User user = new User(); //取的时候取多少次由要查询的字段的个数决定 for(int i = 0;i<columnCount;i++){ //这个地方就没法强转了,没有办法判断具体是什么类型,获取这一行中某一个字段的值 Object columnValue = resultSet.getObject(i + 1); //获取每个列的列名 String columnName = metaData.getColumnName(i + 1); //给user对象指定的columnName属性复制columnValue //利用反射,先拿到叫columnName的属性 Field field = User.class.getDeclaredField(columnName); //这个属性可能是私有的,设置能访问 field.setAccessible(true); //给这个属性赋值 field.set(user,columnValue); } return user; } }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(connection,preparedStatement,resultSet); } return null; } }
为什么是针对一个表的通用查询操作?因为不同的表的字段不同,查询一个表时,除了填写占位符,前面要查询的字段也是变量。
将结果集封装到JavaBean对象中,JavaBean对象的属性名称要和字段名称相同,因为利用反射时,是根据列名在对象中查找属性的域然后赋值。
但是因为数据库和Java中取名习惯不同,如Java中的orderID,在数据库一般取为order_id,这里就需要一步转换。
先将查询的字段起别名:select order_id orderID from demo where age = ?
然后将String columnName = metaData.getColumnName(i + 1);改为String columnLabel = metaData.getColumnLabel(i + 1);
getColumnLabel是返回结果集的别名,如果没有别名则返回列名,所以推荐使用这个方法。
针对于不同表的通用查询操作-不同表返回一个结果
public class Test5 { @Test public void test(){ String sql = "select userName from demo where age = ?"; User user = getInstance(User.class,sql,21); System.out.println(user); } //sql是查询语句,args填充占位符 //Class<T>这个T是运行实例 // <T>T,T是返回值类型,<T>声明泛型方法 public <T>T getInstance(Class<T> clazz,String sql,Object ...args){ Connection connection = null; PreparedStatement preparedStatement = null; ResultSet resultSet = null; try{ //获取连接 connection = JDBCUtils.getConnection(); //预编译sql preparedStatement = connection.prepareStatement(sql); //填写占位符 for(int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行并返回结果集 resultSet = preparedStatement.executeQuery(); ResultSetMetaData metaData = resultSet.getMetaData(); //获取列数 int columnCount = metaData.getColumnCount(); if(resultSet.next()){ //将传入的类创建实例 T t = clazz.newInstance(); //取的时候取多少次由要查询的字段的个数决定 for(int i = 0;i<columnCount;i++){ //这个地方就没法强转了,没有办法判断具体是什么类型,获取这一行中某一个字段的值 Object columnValue = resultSet.getObject(i + 1); //获取每个列的列名 String columnLable = metaData.getColumnLabel(i + 1); //给user对象指定的columnName属性复制columnValue //利用反射,先拿到叫columnName的属性 Field field = clazz.getDeclaredField(columnLable); //这个属性可能是私有的,设置能访问 field.setAccessible(true); //给这个属性赋值 field.set(t,columnValue); } return t; } }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(connection,preparedStatement,resultSet); } return null; } }
用到了反射和泛型。前提都是需要先将JavaBean先创建好。
针对不同表的通用查询操作-返回多个结果
public class Test6 { @Test public void test(){ String sql = "select userName from demo where age = ?"; List<User> list = getForList(User.class, sql, 21); //遍历集合 list.forEach(System.out::println); } public <T> List<T> getForList(Class<T> clazz, String sql, Object ...args){ Connection connection = null; PreparedStatement preparedStatement = null; ResultSet resultSet = null; try{ //获取连接 connection = JDBCUtils.getConnection(); //预编译sql preparedStatement = connection.prepareStatement(sql); //填写占位符 for(int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行并返回结果集 resultSet = preparedStatement.executeQuery(); ResultSetMetaData metaData = resultSet.getMetaData(); //获取列数 int columnCount = metaData.getColumnCount(); //创建集合 List<T> list = new ArrayList<T>(); while (resultSet.next()){ //将传入的类创建实例 T t = clazz.newInstance(); //处理每一行数据的每一列,给每一行看作一个对象,给字段(属性)赋值 for(int i = 0;i<columnCount;i++){ //这个地方就没法强转了,没有办法判断具体是什么类型,获取这一行中某一个字段的值 Object columnValue = resultSet.getObject(i + 1); //获取每个列的列名 String columnLable = metaData.getColumnLabel(i + 1); //给user对象指定的columnName属性复制columnValue //利用反射,先拿到叫columnName的属性 Field field = clazz.getDeclaredField(columnLable); //这个属性可能是私有的,设置能访问 field.setAccessible(true); //给这个属性赋值 field.set(t,columnValue); } list.add(t); } return list; }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(connection,preparedStatement,resultSet); } return null; } }
只在过滤条件处填充占位符。
PreparedStatement的优点
1.解决了sql拼串问题。
2.解决了sql注入问题。
编辑
假如用statement语句去执行这条sql命令,过滤条件会判断为or的关系
而用占位符代替之后,预编译会认为过滤条件是且的关系,之后只填充占位符的内容,如果该内容中有or也无法改变原先且的关系,这就是预编译的好处,所以防止了SQL注入。
3.PreparedStatement操作Blob数据,而Statement做不到
4.PreparedStatement可以实现更高效的批量操作。
假如要添加一万条数据,每条数据都不同,如果用Statement的话,每条数据都要校验格式等,就需要重复一万次,而用预编译的话,由于每条数据的格式都相同,只是占位符的内容不同,所以只需要一次。以后只往占位符填充内容就ok了。
小结
1.在操作数据库时,只需要面向JDBC这套接口编程。
编辑
在用到操作数据库的包中,导入的都是接口,都没有引入第三方的API(也就是各个数据库厂商提供的jar包),那么引用这些jar包的?
编辑
通过拿着这个Driver去获得具体的连接。这个connection就是MySQL的连接。这个connection利用了多态,表面上返回了一个接口,其实这个connection是一个MySQL具体实现类的对象。表面上都是使用的接口,实际上都是具体的MySQL实现类对象。
操作BLOB类型字段
介绍BLOB类型
编辑
BLOB=binary large object, 可以用BLOB类型存图片,视频等。
对BLOB数据的操作
向数据库中存BLOB数据
//向数据库中存BLOB数据 public class BlobTest1 { @Test public void test() throws Exception{ Connection connection = JDBCUtils.getConnection(); String sql = "insert into blobtable values (?,?)"; PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.setObject(1,1); //这个fis就代表了这张图片 FileInputStream fis = new FileInputStream(new File("C:\Users\24213\Pictures\Camera Roll\little cat.webp")); preparedStatement.setBlob(2,fis); preparedStatement.execute(); JDBCUtils.closeResource(connection,preparedStatement,null); } }
向数据库中查找BLOB数据并将数据保存到本地
public class BlobTest2 { //查找BLOB数据 @Test public void test()throws Exception{ Connection connection = JDBCUtils.getConnection(); String sql = "select photo from blobtable where id = ?"; PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1,1); ResultSet resultSet = preparedStatement.executeQuery(); InputStream is = null; FileOutputStream fos = null; if(resultSet.next()){ //将Blob类型的字段数据提取出来 //可以定义成员变量存储,也可以存储到对象中 Blob photo = resultSet.getBlob("photo"); //保存到本地 is = photo.getBinaryStream(); fos = new FileOutputStream("little cat.jpg"); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1){ fos.write(buffer,0,len); } } if(is!=null) is.close(); if(fos!=null) fos.close(); JDBCUtils.closeResource(connection,preparedStatement,resultSet); } }
数据库的批量操作
使用PreparedStatement批量插入操作
update和delete本身具有批量操作的效果,如果不设置过滤条件,那么这个操作默认就是对整体进行。
批量插入操作时Statement和PreparedStatement的对比
编辑
编辑
编辑
优化批量插入操作
进行批处理
public class Test1 { @Test public void test() throws Exception{ long start = System.currentTimeMillis(); Connection connection = JDBCUtils.getConnection(); String sql = "insert into goods values (?,?)"; PreparedStatement preparedStatement = connection.prepareStatement(sql); for (int i = 0;i < 2000;i++){ preparedStatement.setObject(1,i); preparedStatement.setString(2,"good"); //1.攒sql preparedStatement.addBatch(); //每攒500个执行命令就统一执行一次 if(i % 500 == 0){ //2.执行batch preparedStatement.executeBatch(); //3.清空batch preparedStatement.clearBatch(); } } long end = System.currentTimeMillis(); System.out.println(end-start);//5345 //409 JDBCUtils.closeResource(connection,preparedStatement,null); } }
MySQL默认不支持这种操作,需要更改配置文件信息
编辑
未优化前插入两万条数据要5345毫秒,优化后409毫秒。
进一步优化
public class Test1 { @Test public void test() throws Exception{ long start = System.currentTimeMillis(); Connection connection = JDBCUtils.getConnection(); String sql = "insert into goods values (?,?)"; connection.setAutoCommit(false); PreparedStatement preparedStatement = connection.prepareStatement(sql); for (int i = 0;i < 2000;i++){ preparedStatement.setObject(1,i); preparedStatement.setString(2,"good"); //1.攒sql preparedStatement.addBatch(); if(i % 500 == 0){ //2.执行batch preparedStatement.executeBatch(); //3.清空batch preparedStatement.clearBatch(); } } connection.commit(); long end = System.currentTimeMillis(); System.out.println(end-start);//5345 //409 //443 JDBCUtils.closeResource(connection,preparedStatement,null);
设置数据不默认提交,先传送,等着传完了统一提交。因为提交的时候也需要耗费时间。
事务
介绍
编辑
一个工作单元:一个或多个DML操作。
数据一旦提交,就不能回滚了。也就是说不能回到原来未处理的状态了。
事务的使用
编辑
哪些操作会导致数据的自动提交?
1.DDL操作一旦执行都会自动提交。不能修改。
2.DML默认情况下,一旦提交,就会自动提交。可以通过set autocommit=false方式取消DML操作自动提交。
3.默认在关闭连接时会自动提交数据。
例子
public static void main(String[] args) { String sql1 = "update deposit set balance = balance - 100 where id = ?"; update(sql1,1); System.out.println(10/0); String sql2 = "update deposit set balance = balance + 100 where id = ?"; update(sql2,2); //update方法是前面的通用增删改操作 }
这样就会导致1号balance减少100而2号balance没有变。
要避免这种情况,就需要防止默认提交,不然就不能回滚,就要避免上面三种情况。
修改之前通用的增删改操作
@Test public class OperationOfCUD_transaction { public void test(){ Connection connection = null; try{ connection = JDBCUtils.getConnection(); //取消DML操作的自动提交 connection.setAutoCommit(false); String sql1 = "update deposit set balance = balance - 100 where id = ?"; update(connection,sql1,1); //System.out.println(10/0); String sql2 = "update deposit set balance = balance + 100 where id = ?"; update(connection,sql2,2); System.out.println("转账成功"); connection.commit(); }catch (Exception e){ e.printStackTrace(); //如果中途碰到异常就进入catch里面,在这里面就需要回滚 try { connection.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } }finally { JDBCUtils.closeResource(connection,null,null); } } //不能在update里面创建连接并关闭连接,因为关闭连接会默认自动提交事务。 public static int update(Connection connection,String sql,Object ...args){//可变形参 PreparedStatement preparedStatement = null; try{ //预编译sql preparedStatement = connection.prepareStatement(sql); //填充占位符 //遍历可变数组,数组的内容就是传进来填写占位符的数据 for (int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行 return preparedStatement.executeUpdate(); }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(null,preparedStatement,null); } return 0; } }
connection要在通用操作外面出现
编辑
为了确保事务的提交自己能控制,在目的操作完成之后再提交,操作若完成不了则能回滚,就要防止事务的默认提交。
首先自己创建connection自己创建,不要在DML操作里创建,因为DML里面connection要关闭,当有多个DML操作时,如果在某个DML内提交了,下一个DML出现错误就不能回滚到最开始了。
其次要设置DML操作完成后默认不提交。
最后让connection将所有DML操作串联起来,最后再手动将connection关闭。
事务的ACID属性
编辑
原子性:事务就像原子一样不可再分,一个事务要么发生要么不发生,不能发生一部分。
隔离性:一个事务在操作一张表时,另一个事务不能操作, 就像处理线程安全一样。
数据库的并发问题
编辑
DAO及其实现类
DAO:database access object 数据库访问对象
作用:封装了针对于数据表的通用的操作。增删改查这些操作。
BaseDAO:提供数据库访问操作
BaseDAO中没有抽象方法,但这个类需要是抽象类,因为不需要这个类创建实例。
public abstract class BaseDAO { //增删改 public static int update(Connection connection, String sql, Object ...args){//可变形参 PreparedStatement preparedStatement = null; try{ //预编译sql preparedStatement = connection.prepareStatement(sql); //填充占位符 //遍历可变数组,数组的内容就是传进来填写占位符的数据 for (int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行 return preparedStatement.executeUpdate(); }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(null,preparedStatement,null); } return 0; } //查询一个对象 public <T>T getInstance(Connection connection,Class<T> clazz,String sql,Object ...args){ PreparedStatement preparedStatement = null; ResultSet resultSet = null; try{ //获取连接 connection = JDBCUtils.getConnection(); //预编译sql preparedStatement = connection.prepareStatement(sql); //填写占位符 for(int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行并返回结果集 resultSet = preparedStatement.executeQuery(); ResultSetMetaData metaData = resultSet.getMetaData(); //获取列数 int columnCount = metaData.getColumnCount(); if(resultSet.next()){ //将传入的类创建实例 T t = clazz.newInstance(); //取的时候取多少次由要查询的字段的个数决定 for(int i = 0;i<columnCount;i++){ //这个地方就没法强转了,没有办法判断具体是什么类型,获取这一行中某一个字段的值 Object columnValue = resultSet.getObject(i + 1); //获取每个列的列名 String columnLable = metaData.getColumnLabel(i + 1); //给user对象指定的columnName属性复制columnValue //利用反射,先拿到叫columnName的属性 Field field = clazz.getDeclaredField(columnLable); //这个属性可能是私有的,设置能访问 field.setAccessible(true); //给这个属性赋值 field.set(t,columnValue); } return t; } }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(null,preparedStatement,resultSet); } return null; } //查询多个对象 public <T> List<T> getForList(Connection connection,Class<T> clazz, String sql, Object ...args){ PreparedStatement preparedStatement = null; ResultSet resultSet = null; try{ //获取连接 connection = JDBCUtils.getConnection(); //预编译sql preparedStatement = connection.prepareStatement(sql); //填写占位符 for(int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行并返回结果集 resultSet = preparedStatement.executeQuery(); ResultSetMetaData metaData = resultSet.getMetaData(); //获取列数 int columnCount = metaData.getColumnCount(); //创建集合 List<T> list = new ArrayList<T>(); while (resultSet.next()){ //将传入的类创建实例 T t = clazz.newInstance(); //处理每一行数据的每一列,给每一行看作一个对象,给字段(属性)赋值 for(int i = 0;i<columnCount;i++){ //这个地方就没法强转了,没有办法判断具体是什么类型,获取这一行中某一个字段的值 Object columnValue = resultSet.getObject(i + 1); //获取每个列的列名 String columnLable = metaData.getColumnLabel(i + 1); //给user对象指定的columnName属性复制columnValue //利用反射,先拿到叫columnName的属性 Field field = clazz.getDeclaredField(columnLable); //这个属性可能是私有的,设置能访问 field.setAccessible(true); //给这个属性赋值 field.set(t,columnValue); } list.add(t); } return list; }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(null,preparedStatement,resultSet); } return null; } //聚合函数的查询 //查最大值,最小值,数量 //有多种类型,比如可以查员工中最大的生日,就是Date类型 public <E>E getValue(Connection connection,String sql,Object ...args){ PreparedStatement preparedStatement = null; ResultSet resultSet = null; try{ preparedStatement = connection.prepareStatement(sql); for (int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } resultSet = preparedStatement.executeQuery(); if(resultSet.next()){ return (E) resultSet.getObject(1); } }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(null,preparedStatement,resultSet); } return null; } }
针对于操作不同的表,需要创建不同的内容。
下面以查询customer表为例。首先需要有关于这个表各个字段的JavaBean。
其次创建接口和实现类
//此接口用于规范针对customer表的常用操作 //对于customer表想要提供的功能 public interface CustomerDAO { //将customer添加到数据库中 void insert(Connection connection,Customer customer); //针对指定id,删除表中一条记录 void deleteById(Connection connection,int id); //针对内存中的customer,去修改某个用户的记录 void update(Connection connection,Customer customer); //针对指定的id查询对应的Customer对象 Customer getCustomerById(Connection connection,int id); //查询表中所有记录 List<Customer> getAll(Connection connection); //返回数据表中数据的条目数 Long getCount(Connection connection); //根据不同的表有不同的需求,这里可以返回最大的生日 Date getMaxBirth(Connection connection); }
public class CustomerDAOImpl extends BaseDAO implements CustomerDAO { @Override public void insert(Connection connection, Customer customer) { } @Override public void deleteById(Connection connection, int id) { } @Override public void update(Connection connection, Customer customer) { } @Override public Customer getCustomerById(Connection connection, int id) { return null; } @Override public List<Customer> getAll(Connection connection) { return null; } @Override public Long getCount(Connection connection) { return null; } @Override public Date getMaxBirth(Connection connection) { return null; } }
当需要查询其他表时,就创建新的接口和新的实现类。
下面是查询customer表的完整DAO代码
BaseDAO
public abstract class BaseDAO { //增删改 public static int update(Connection connection, String sql, Object ...args){//可变形参 PreparedStatement preparedStatement = null; try{ //预编译sql preparedStatement = connection.prepareStatement(sql); //填充占位符 //遍历可变数组,数组的内容就是传进来填写占位符的数据 for (int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行 return preparedStatement.executeUpdate(); }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(null,preparedStatement,null); } return 0; } //查询一个对象 public <T>T getInstance(Connection connection,Class<T> clazz,String sql,Object ...args){ PreparedStatement preparedStatement = null; ResultSet resultSet = null; try{ //获取连接 connection = JDBCUtils.getConnection(); //预编译sql preparedStatement = connection.prepareStatement(sql); //填写占位符 for(int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行并返回结果集 resultSet = preparedStatement.executeQuery(); ResultSetMetaData metaData = resultSet.getMetaData(); //获取列数 int columnCount = metaData.getColumnCount(); if(resultSet.next()){ //将传入的类创建实例 T t = clazz.newInstance(); //取的时候取多少次由要查询的字段的个数决定 for(int i = 0;i<columnCount;i++){ //这个地方就没法强转了,没有办法判断具体是什么类型,获取这一行中某一个字段的值 Object columnValue = resultSet.getObject(i + 1); //获取每个列的列名 String columnLable = metaData.getColumnLabel(i + 1); //给user对象指定的columnName属性复制columnValue //利用反射,先拿到叫columnName的属性 Field field = clazz.getDeclaredField(columnLable); //这个属性可能是私有的,设置能访问 field.setAccessible(true); //给这个属性赋值 field.set(t,columnValue); } return t; } }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(null,preparedStatement,resultSet); } return null; } //查询多个对象 public <T> List<T> getForList(Connection connection,Class<T> clazz, String sql, Object ...args){ PreparedStatement preparedStatement = null; ResultSet resultSet = null; try{ //获取连接 connection = JDBCUtils.getConnection(); //预编译sql preparedStatement = connection.prepareStatement(sql); //填写占位符 for(int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } //执行并返回结果集 resultSet = preparedStatement.executeQuery(); ResultSetMetaData metaData = resultSet.getMetaData(); //获取列数 int columnCount = metaData.getColumnCount(); //创建集合 List<T> list = new ArrayList<T>(); while (resultSet.next()){ //将传入的类创建实例 T t = clazz.newInstance(); //处理每一行数据的每一列,给每一行看作一个对象,给字段(属性)赋值 for(int i = 0;i<columnCount;i++){ //这个地方就没法强转了,没有办法判断具体是什么类型,获取这一行中某一个字段的值 Object columnValue = resultSet.getObject(i + 1); //获取每个列的列名 String columnLable = metaData.getColumnLabel(i + 1); //给user对象指定的columnName属性复制columnValue //利用反射,先拿到叫columnName的属性 Field field = clazz.getDeclaredField(columnLable); //这个属性可能是私有的,设置能访问 field.setAccessible(true); //给这个属性赋值 field.set(t,columnValue); } list.add(t); } return list; }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(null,preparedStatement,resultSet); } return null; } //聚合函数的查询 //查最大值,最小值,数量 //有多种类型,比如可以查员工中最大的生日,就是Date类型 public <E>E getValue(Connection connection,String sql,Object ...args){ PreparedStatement preparedStatement = null; ResultSet resultSet = null; try{ preparedStatement = connection.prepareStatement(sql); for (int i = 0;i < args.length;i++){ preparedStatement.setObject(i+1,args[i]); } resultSet = preparedStatement.executeQuery(); if(resultSet.next()){ return (E) resultSet.getObject(1); } }catch (Exception e){ e.printStackTrace(); }finally { JDBCUtils.closeResource(null,preparedStatement,resultSet); } return null; } }
表的接口,定义对这个表操作的规范
//此接口用于规范针对customer表的常用操作 //对于customer表想要提供的功能 public interface CustomerDAO { //将customer添加到数据库中 void insert(Connection connection,Customer customer); //针对指定id,删除表中一条记录 void deleteById(Connection connection,int id); //针对内存中的customer,去修改某个用户的记录 void update(Connection connection,Customer customer); //针对指定的id查询对应的Customer对象 Customer getCustomerById(Connection connection,int id); //查询表中所有记录 List<Customer> getAll(Connection connection); //返回数据表中数据的条目数 Long getCount(Connection connection); //根据不同的表有不同的需求,这里可以返回最大的生日 Date getMaxBirth(Connection connection); }
对customer表的操作
public class CustomerDAOImpl extends BaseDAO implements CustomerDAO { @Override public void insert(Connection connection, Customer customer) { String sql = "insert into customers (name,email,birth) values (?,?,?)"; update(connection,sql,customer.getName(),customer.getEmail(),customer.getBirth()); } @Override public void deleteById(Connection connection, int id) { String sql = "delete from customers where id = ?"; update(connection,sql,id); } @Override public void update(Connection connection, Customer customer) { String sql = "update customers set name = ?,email = ?,birth = ? where id = ?"; update(connection,sql,customer.getName(),customer.getEmail(),customer.getBirth(),customer.getId()); } @Override public Customer getCustomerById(Connection connection, int id) { String sql = "select id,name,email,birth from customers where id = ?"; Customer customer = getInstance(connection, Customer.class, sql, id); return customer; } @Override public List<Customer> getAll(Connection connection) { String sql = "select id,name,email,birth from customers"; List<Customer> list = getForList(connection,Customer.class,sql); return list; } @Override public Long getCount(Connection connection) { String sql = "select count(*) from customers"; return getValue(connection,sql); } @Override public Date getMaxBirth(Connection connection) { String sql = "select max(birth) from customers"; return getValue(connection,sql); } }
测试一下
public class TestDAO { public static void main(String[] args) { CustomerDAOImpl customerDAO = new CustomerDAOImpl(); Connection connection = JDBCUtils.getConnection(); Customer customer = new Customer(1,"小范","123@qq.com",new Date(2001320)); //测试点1 //customerDAO.insert(connection,customer); //JDBCUtils.closeResource(connection,null,null); //测试点2 //customerDAO.deleteById(connection,2); //测试点3 //customerDAO.update(connection,customer); //测试点4 //Customer customer1 = customerDAO.getCustomerById(connection, 1); //System.out.println(customer1); //测试点5 //List<Customer> all = customerDAO.getAll(connection); //System.out.println(all); //测试点6 //System.out.println(customerDAO.getCount(connection)); //测试点7 //System.out.println(customerDAO.getMaxBirth(connection)); //全通过 } }
数据库连接池
JDBC数据库连接池的必要性
编辑
内存泄漏
C的内存泄漏:指针指向一块内存,如果这个指针丢了,就不能回收这个内存了。
Java的内存泄漏:内存中有对象不能被回收。如果连接一直存在没有及时关闭就是出现了内存泄漏了。
数据库连接池技术介绍
编辑
编辑
可以让连接进行重用。
如果你想去某个地方,按照原先创建连接的思想,你会先造一辆自行车,然后到了目的地,再把自行车销毁。显然不能达到资源的重复利用。
数据库连接池的思想就像是Hello单车一样,想骑车子就去停放点去取车,骑完再放回去让别人骑。
编辑
编辑
多种开源的数据库连接池
编辑
编辑
编辑
编辑
在xml配置文件中写上key-value运行的时候就会自动去找这个key对应的value去获取连接。
\