【Java Se】JDBC


启停服务
net start mysql
net stop mysql

登录

mysql -u -p

访问指定IP的mysql

mysql -u root -P 3306 -h localhost -pabc123

-p后面不能加空格

-h表示host,-h-P均为默认本机

两种架构

B/S:Browser Server

C/S:Client Server

1 JDBC(Java Database Connectivity)

是一个独立于特定数据库管理系统、通用的SQL数据库存储和操作的公共接口,接口封装了一系列抽象方法,定义了用来访问数据库的标准Java类库(java.sql.javax.sql),使用这些类库可以以一种比较标准的方法,方便地访问数据库资源。

2 JDBC程序编写步骤

2.1 获取数据库连接方式一:

2.1.1 Driver接口实现类

java.sql.Driver接口是所有JDBC驱动程序需要实现的接口。由数据库厂商提供,不同数据库厂商提供不同的实现

Driver driver = new com.mysql.jdbc.Driver();
2.1.2 URL
String url = "jdbc:mysql://localhost:3306/mydb";

jdbc:协议

mysql:子协议

localhost:ip

3306:端口号

mydb:数据库名称

?:可添加关键字

2.1.3 info
        Properties info = new Properties();
        info.setProperty("user", "root");
        info.setProperty("password", "abc123");

用一个Properities封装数据库的用户名和密码

2.1.4 获得连接
        Connection connection = driver.connect(url, info);

合起来:

    @Test
    public void testConnection() throws SQLException {
        Driver driver = new com.mysql.jdbc.Driver();

        String url = "jdbc:mysql://localhost:3306/mydb";
        Properties info = new Properties();
        info.setProperty("user", "root");
        info.setProperty("password", "abc123");

        Connection connection = driver.connect(url, info);

        System.out.println(connection);
    }

2.2 方式二  :利用反射对方式一进行迭代

    @Test
    public void testConnection1() throws ClassNotFoundException, IllegalAccessException, InstantiationException, SQLException {
        Class clazz = Class.forName("com.mysql.jdbc.Driver");
        Driver driver = (Driver) clazz.newInstance();

        String url = "jdbc:mysql://localhost:3306/mydb";
        Properties properties = new Properties();
        properties.setProperty("user", "root");
        properties.setProperty("password", "abc123");

        Connection connection = driver.connect(url, properties);
    }

利用反射的动态性动态获取数据库驱动Driver实现类

2.3 方式三:使用DriverManager替换Driver

    @Test
    public void testConnection2() throws ClassNotFoundException, IllegalAccessException, InstantiationException, SQLException {
        Class clazz = Class.forName("com.mysql.jdbc.Driver");
        Driver driver = (Driver) clazz.newInstance();
        DriverManager.registerDriver(driver);

        String url = "jdbc:mysql://localhost:3306/mydb";
        String user = "root";
        String password = "abc123";

        Connection connection = DriverManager.getConnection(url, user, password);
        System.out.println(connection);
    }

DriverManager.registerDriver(driver):注册驱动

2. 4 方式四:可以只是加载驱动,不用显示地注册驱动了

    @Test
    public void testConnection3() throws ClassNotFoundException, IllegalAccessException, InstantiationException, SQLException {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String user = "root";
        String password = "abc123";

        Class.forName("com.mysql.jdbc.Driver");

        Connection connection = DriverManager.getConnection(url, user, password);
        System.out.println(connection);
    }

这一方面是由于反射机制会加载Driver类到内存,另一方面Driver父类com.mysql.cj.jdbc.Driver中存在静态代码块,在加载到内存时就进行了隐式地注册。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }

}
2.5 方式五(伪):java.sql.Driver文件在jar包导入时就加载了Driver父类com.mysql.cj.jdbc.Driver
    @Test
    public void testConnection4() throws ClassNotFoundException, IllegalAccessException, InstantiationException, SQLException {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String user = "root";
        String password = "abc123";

//        Class.forName("com.mysql.jdbc.Driver");

        Connection connection = DriverManager.getConnection(url, user, password);
        System.out.println(connection);
    }

2.5 方式五(final版)配置文件

    @Test
    public void testConnection5() throws IOException, ClassNotFoundException, SQLException {
        //1.读取配置文件的四个信息:通过类的系统类加载器读取配置文件
        InputStream inputStream = ConnectionTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
        Properties properties = new Properties();
        properties.load(inputStream);

        String url = properties.getProperty("url");
        String user = properties.getProperty("user");
        String password = properties.getProperty("password");
        String driverClass = properties.getProperty("driverClass");
        //2.加载驱动
        Class.forName(driverClass);
        //3.获得连接
        Connection connection = DriverManager.getConnection(url, user, password);
        System.out.println(connection);
    }

实现了数据和代码的分离,实现了解耦

如果需要修改配置文件信息,可以避免项目重新打包

3 操作和访问数据库

数据库连接用于向数据库发送命令和SQL语句,并接受数据库服务器返回的结果。其实一个数据库连接就是一个Socket连接。

在java.sql包中有3个接口分别定义了对数据库的调用的不同方式:

3.1 Statement:用于向数据库发送SQL语句

    @Test
    public void testLogin() throws IOException, ClassNotFoundException, SQLException {
        InputStream resourceAsStream = StatementTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
        Properties info = new Properties();
        info.load(resourceAsStream);

        String url = info.getProperty("url");
        String user = info.getProperty("user");
        String pwd = info.getProperty("password");
        String driverClass = info.getProperty("driverClass");

        Class.forName(driverClass);
        Connection connection = DriverManager.getConnection(url, user, pwd);
        String sql = "SELECT * FROM USER WHERE uer = 'AA' AND pwd = '123456'";

        Statement statement = connection.createStatement();
        ResultSet rs = statement.executeQuery(sql);

        while(rs.next()) {
            System.out.println(rs.getString(1) + " " + rs.getString(2));
        }

    }

Statement的缺陷

①需要拼写sql语句

②存在sql注入问题:利用某些系统没有对用户数据进行充分的检查,在用户输入的数据组注入非法的SQL语句段或命令,从而利用SQL引擎完成恶意行为的做法。

SELECT * FROM USER WHERE uer = '1' OR ' AND pwd = ' =1 OR '1' = '1'

③导致恶意攻击

3.2 PrepatedStatement:SQL语句被预编译并存储在此对象中,可以使用此对象多次高效地执行该语句

    @Test
    public void preparedStatementTest() throws ClassNotFoundException, SQLException, IOException {
        InputStream resourceAsStream = StatementTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
        Properties info = new Properties();
        info.load(resourceAsStream);

        String url = info.getProperty("url");
        String user = info.getProperty("user");
        String pwd = info.getProperty("password");
        String driverClass = info.getProperty("driverClass");

        Class.forName(driverClass);
        Connection connection = DriverManager.getConnection(url, user, pwd);

        String sql = "INSERT INTO USER(uer, pwd) VALUES (?, ?)";
        PreparedStatement ps = connection.prepareStatement(sql);

        ps.setString(1, "111111");
        ps.setString(2, "111111");

        boolean result = ps.execute();

        ps.close();
        connection.close();

        System.out.println(result);
    }

execute方法返回一个boolean值,如果第一个结果是ResultSet对象则返回true,如果是更新计数或者没有返回结果则返回false。所以执行插入或者更新语句应该使用executeUpdate。

用于获取关闭连接的JDBCUtils工具类:

public class JDBCUtils {
    public static Connection getConnection() {
        Connection conn = null;
        InputStream resourceAsStream = null;
        try {
            resourceAsStream = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
            Properties info = new Properties();
            info.load(resourceAsStream);

            String user = info.getProperty("user");
            String password = info.getProperty("password");
            String url = info.getProperty("url");
            String driverClass = info.getProperty("driverClass");

            Class.forName(driverClass);
            conn = DriverManager.getConnection(url, user, password);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
        }

        return conn;
    }
    public static void resourceClose(Connection conn, Statement statement) {
        try {
            conn.close();
            statement.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

}

可用作实现增删改的通用方法:

public void update(String sql, Object ...args) {
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = JDBCUtils.getConnection();
            ps = conn.prepareStatement(sql);

            for(int i=0;i

测试:

    @Test
    public void testUpdate() throws ParseException {
        String sql = "insert into user(uer,pwd,birthday) values(?,?,?)";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        java.util.Date date = sdf.parse("1997-05-30");
        java.sql.Date birthday =new Date(date.getTime());

//        System.out.println(birthday);
        update(sql, "my", "pwdd", birthday);
    }

注意这里通过sql.Date毫秒数的构造器实现了util.Date与sql.Date的转换:

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        java.util.Date date = sdf.parse("1997-05-30");
        java.sql.Date birthday =new Date(date.getTime());

ResultSet查询结果集

    @Test
    public void queryTest() {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        String sql = "select uer, pwd, birthday from user where id=?";

        try {
            conn = JDBCUtils.getConnection();
            ps = conn.prepareStatement(sql);
            ps.setInt(1, 14);
            rs = ps.executeQuery();

            while(rs.next()) {
                String user = rs.getString(1);
                String pwd = rs.getString(2);

                System.out.println(user + ", " + pwd);
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils.resourceClose(conn, ps, rs);
        }
    }

区别于迭代器的next,结果集的next方法判断为空的同时还进行了指针下移

★★★★★利用反射实现通用查询操作:(ResultSetMeta确定字段列数和列名)
public User query(String sql, Object ...args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        User user = null;
        try {
            conn = JDBCUtils.getConnection();
            ps = conn.prepareStatement(sql);

            for(int i=0;i

这里只能使用反射Field的set方法,使用Method的invoke方法不能确定参数类型(实际也能通过ResultSetMetaData获取数据库字段类型对应java数据类型,但是很麻烦)

    @Test
    public void queryTest1() {
        String sql = "select uer,pwd,birthday from User where id = ?";
        User user =  query(sql, 14);
        System.out.println(user);
    }

针对数据库字段名和java类属性名不一致的情况:

①必须使用java类的属性名命名字段的别名

String sql = "select user_uer uer,user_pwd pwd,user_birthday birthday from User where id = ?";

②ResultSetMetaData的getColumnName()方法获取的是数据库的字段名,getColumnLabel()方法获取的是别名

★利用泛型方法实现对不同表的查询操作

    public  T query(Class clazz, String sql, Object ...args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        T t = null;
        try {
            conn = JDBCUtils.getConnection();
            ps = conn.prepareStatement(sql);

            for(int i=0;i

PreparedStatement为什么能防止SQL注入?

PreparedStatement实行预编译,在还没有填充占位符的时候就确定了sql语句的逻辑结构。

PreparedStatement可以使用流填充,因此可以操作Blob型数据向数据库传输视频、图片等。

PreparedStatement可以实现更高效的批量操作:对于sql语句只需要校验一次然后缓存到PreparedStatement对象中,之后只需要对占位符进行填充即可。

Blob数据

    @Test
    public void BlobTest() {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            conn = JDBCUtils.getConnection();
            String sql = "select photo from user where id=23";
            ps = conn.prepareStatement(sql);
            rs = ps.executeQuery();

            while(rs.next()) {

                Blob blob= rs.getBlob(1);

                InputStream is = blob.getBinaryStream();
                FileOutputStream fos = new FileOutputStream("2B.jpg");
                byte[] buffer = new byte[1024];
                int len;
                while((len = is.read(buffer)) != -1) {
                    fos.write(buffer, 0, len);
                }
            }
        } catch (SQLException | FileNotFoundException throwables) {
            throwables.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }
    }
类型 大小(字节)
TinyBlob 255
Blob 65K
MediumBlob 16M
LongBlob 4G

批量插入数据

①update、delete本身就具有批量操作的能力。此处的批量操作,主要是指批量插入:

@Test
    public void testInsert1() {
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = JDBCUtils.getConnection();
            String sql = "insert into goods(name) values(?)";
//            boolean flag = true;
            for(int i=0;i<20000;i++) {
                ps = conn.prepareStatement(sql);
                String str = "goods" + (i + 1);
                ps.setString(1, str);
                ps.executeUpdate();
            }

        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            try {
                if(conn != null) {
                    conn.close();
                }
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
            try {
                if(ps != null) {
                    ps.close();
                }
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
    }

②此种方式每次填充完占位符都需要与数据库交互,效率较低,可以存积一定数量ps数据库操作再与数据库交互:

            for(int i=0;i<20000;i++) {
                String str = "goods" + (i + 1);
                ps.setString(1, str);
                ps.addBatch();
                if((i+1)%500==0) {
                    ps.executeBatch();
                    ps.clearBatch();
                }
            }

mysql默认服务器是关闭批处理的,我们需要一个在配置文件中加一个url参数:

url=jdbc:mysql://localhost:3306/mydb?rewriteBatchedStatements=true

时间由20s变为277ms

③conn.setAutoCommit(false):设置连接不可自动提交数据

            conn = JDBCUtils.getConnection();
            conn.setAutoCommit(false);
            String sql = "insert into goods(name) values(?)";
            ps = conn.prepareStatement(sql);
//            boolean flag = true;
            long stime = System.currentTimeMillis();
            for(int i=0;i<1000000;i++) {
                String str = "goods" + (i + 1);
                ps.setString(1, str);
                ps.addBatch();
                if((i+1)%500==0) {
                    ps.executeBatch();
                    ps.clearBatch();
                }
            }
            conn.commit();
            long etime = System.currentTimeMillis();
            System.out.println(etime - stime);

3.3 数据库事务

事务:一组逻辑单元,使数据库从一种状态变换到另一种状态。

事务处理:保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种工作方式。当一个事务中执行多个操作,要么所有的事务都被提交(commit),那么这些修改就被永久地保存下来,要么数据库放弃所有修改,整个事务回滚(rollback) 到最初状态。

哪些操作会导致数据库的自动提交?

①DDL操作一旦执行,都会自动提交,set autocommit = false对DDL操作无效。

②DML默认情况会自动提交,可以通过set autocommit = false的方式取消DML操作的自动提交。

DML(Data Manipulation Language)数据操纵语言:

适用范围:对数据库中的数据进行一些简单操作,如insert,delete,update,select等.

DDL(Data Definition Language)数据定义语言:

适用范围:对数据库中的某些对象(例如,database,table)进行管理,如Create,Alter和Drop.

    @Test
    public void updateTest() {

        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            conn.setAutoCommit(false);

//            System.out.println(conn.getAutoCommit());

            String sql1 = "update user set balance = balance - 100 where uer=?";
            update(conn, sql1, "AA");

            System.out.println(1/0);

            String sql2 = "update user set balance = balance + 100 where uer=?";
            update(conn, sql2, "2B");

            System.out.println("转账成功");
            conn.commit();

        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            try {
                if(conn!=null) {
                    conn.close();
                }
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }


    }

##### ※ 代码块在子类对象生成前获取父类的泛型参数,

private Class clazz = null;

{
    //代码块在子类对象生成前获取父类的泛型参数
    Type genericSuperclass = this.getClass().getGenericSuperclass();
    ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;

    Type[] typeArguments = parameterizedType.getActualTypeArguments();
    clazz = (Class) typeArguments[0];

}
这么做的目的是为了不用因为反射需要,每次都写明类型

public List getForList(Connection conn, String sql, Object...args) {
List list = new ArrayList<>();
PreparedStatement ps = null;
T t = null;
ResultSet rs = null;

    try {
        ps = conn.prepareStatement(sql);

        for(int i=0;i

4 数据库连接池

在使开发基于数据库的web程序时,基本按照以下步骤:先在主程序中建立数据库连接,然后执行sql操作,最后断开sql连接。

这种开发模式存在的问题:

普通的JDBC数据库连接使用DriverManager获取,每次连接数据库都需要将Connection加载到内存,并且每次都需要验证用户名密码花费时间。数据库连接资源没有得到很好的重复利用。

每次连接使用完后都得断开,如果程序出现异常没有关闭,将会导致数据库系统中的内存泄漏,最终导致重启数据库。

内存对象不能被JVM回收的情况,称为内存泄漏

这种开发不能控制被创建的连接对象数,系统资源毫无顾忌地分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。

4.1 多种开源的数据库连接池

JDBC的数据库连接池java.sql.DataSource来表示,DataSource只是一个接口,该接口由服务器(Weblogic,WebSphere,Tomcat)提供实现。

C3P0

方式一:硬编码方式创建数据库连接池

    @Test
    public void test() throws Exception {
        ComboPooledDataSource cpds = new ComboPooledDataSource();
        cpds.setDriverClass( "com.mysql.cj.jdbc.Driver" ); //loads the jdbc driver
        cpds.setJdbcUrl( "jdbc:mysql://localhost/mydb" );
        cpds.setUser("root");
        cpds.setPassword("abc123");
        //设置初始时数据库连接池的参数
        cpds.setInitialPoolSize(10);

        Connection connection = cpds.getConnection();
        System.out.println(connection);
    }

方式二:通过配置文件设置数据库连接池


    
        
        com.mysql.cj.jdbc.Driver
        jdbc:mysql://localhost:3306/mydb
        root
        abc123

        

        
        5
        
        10
        
        10
        
        100
        
        0
        
        5

    

获取连接

    @Test
    public void test1() throws SQLException {
        ComboPooledDataSource cpds = new ComboPooledDataSource("HelloC3P0");
        Connection conn = cpds.getConnection();
        System.out.println(conn);
    }
DBCP
Druid(德鲁伊)数据库连接池

配置文件

url=jdbc:mysql://localhost:3306/mydb
username=root
password=abc123
driverClassName=com.mysql.cj.jdbc.Driver

initialSize=10
maxActive=10
    @Test
    public void test2() throws Exception {
        Properties properties = new Properties();
        InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties");
        properties.load(resourceAsStream);

        DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);

        Connection conn = dataSource.getConnection();
        System.out.println(conn);
    }

5 Apache DBUTILS

commons-dbutils是Apache组织提供的一个开源JDBC工具类库,封装了针对于数据库的增删改查操作。

DButils增删改操作
    @Test
    public void test() {
        Connection conn = null;
        try {
            QueryRunner qr = new QueryRunner();
            conn = JDBCUtils.getConnection();
            String sql = "insert into user(user,balance) values(?,?)";
            int insert_count = qr.update(conn, sql, "1A", 100000);
            System.out.println(insert_count);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils.resourceClose(conn, null);
        }

    }
DButils查询操作

runner参数包括:

query(Connection conn, String sql, ResultSetHandler rsh, Object... params)

其中ResultSetHandler为接口,需要创建一个接口实现类,可供使用的接口实现类有:

AbstractKeyedHandler, AbstractListHandler, ArrayHandler, ArrayListHandler, BeanHandler, BeanListHandler, ColumnListHandler, KeyedHandler, MapHandler, MapListHandler, ScalarHandler

BeanListHandler 用于返回一条数据
       BeanHandler handler = new BeanHandler<>(User.class)
    @Test
    public void test1() {
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            QueryRunner runner = new QueryRunner();
            String sql = "select user,balance from user where id = ?";
            BeanHandler handler = new BeanHandler<>(User.class);

            User user = runner.query(conn, sql, handler, 1);
            System.out.println(user);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils.resourceClose(conn, null);
        }
    }
BeanListHandler用于返回多条数据
BeanListHandler handler = new BeanListHandler<>(User.class);
    @Test
    public void test2() {
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            QueryRunner runner = new QueryRunner();
            String sql = "select user,balance from user";
            BeanListHandler handler = new BeanListHandler<>(User.class);

            List list = runner.query(conn, sql, handler);
            list.forEach(System.out::println);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils.resourceClose(conn, null);
        }
    }

MapListHandler将字段作为key,字段值为value返回map组成的list

    @Test
    public void test3() {
        Connection conn = null;

        try {
            conn = JDBCUtils.getConnection();
            QueryRunner runner = new QueryRunner();
            String sql = "select user, balance from user";

            MapListHandler handler = new MapListHandler();
            List> list = runner.query(conn, sql, handler);

            list.forEach(System.out::println);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils.resourceClose(conn, null);
        }
    }
ScalarHandler用于特殊查询
    @Test
    public void test4() {
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            QueryRunner runner = new QueryRunner();
            String sql = "select count(*) from user";

            ScalarHandler handler = new ScalarHandler();
            Long count = (Long) runner.query(conn, sql, handler);

            System.out.println(count);

        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils.resourceClose(conn, null);
        }
    }

这里只能用Long