JDBC学习笔记(2)

236 阅读23分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

概述

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去获取连接。

\