彻底理解 JDBC

想要更好的阅读体验,可以转我的个人博客: nonlinearthink

JDBC (Java Database Connectivity) ,属于 Java 应用编程中比较基础的一块,我们会从最基本的开始,由浅入深地解释 JDBC 中的各种问题。

连接数据库

驱动初始化

基于 Class.forName 的初始化

想要开始 JDBC 编程,第一步是需要把 数据库驱动程序 的代码加载进来。

可以利用 Class.forName 函数,它原本的功能是返回一个 类或者接口的 Class 对象 ,也就是相当于初始化了一个类,一般用它执行这个类的静态代码段

try {
    Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

这样,com.mysql.jdbc.Driver 类中的静态代码段就会被执行,进行初始化。

不过,驱动初始化还有另外一种写法。

基于 registerDriver 的初始化

我们知道 Class.forName 是执行类的静态代码段,那我们把 com.mysql.jdbc.Driver 里面的静态代码段照着样子抄一遍不也可以实现初始化吗?

这是 com.mysql.jdbc.Driver 静态代码段的源代码:

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

这段代码可以完全代替上一小节的代码,我们可以认为 DriverManager.registerDriver(new Driver())Class.forName 的写法是等效的。

什么是 数据库驱动程序

JDK 提供了一个 JDBC 的接口(Interface)。但是,因为是接口,所以还是没有实现的,每个数据库的厂商需要自己实现这个接口,这样用户才能正常调用。

以 mysql 为例,下载地址: dev.mysql.com/downloads/connector/j

接口变动

mysql5 和 mysql6 的驱动程序接口有区别,从 com.mysql.jdbc.Driver 换成了 com.mysql.cj.jdbc.Driver,再往上的版本同 mysql6 。

建立连接

数据库打开后会在本地开一个端口,运行进程,我们可以通过这个端口的 URL 来访问数据库。

当然,还需要数据库的用户名和密码。

java.sql.DriverManager.getConnection(url, user, password);

JDBC 的 URL 格式: jdbc:[数据库连接名]://localhost:[端口号]/[数据库名]

  • 数据库连接名
    mysql、sqlserver
  • 端口号
    3306(mysql)、1433(sqlserver)
  • 数据库名
    业务相关的数据库名,自定义

一个 JDBC 连接数据库的例子:

try {
    java.sql.Connection conn = java.sql.DriverManager.getConnection("jdbc:mysql://localhost:3306/booklib",
            "root", "123456");
} catch (SQLException e) {
    e.printStackTrace();
}

JDBC 基础

Statement

Statement 可以根据给出的一条 SQL 字符串,然后调用运行。

借用上一节连接得到的 conn 对象,它有一个 createStatement 函数,可以创建一个 Statement。

try {
    java.sql.Connection conn = java.sql.DriverManager.getConnection("jdbc:mysql://localhost:3306/booklib",
            "root", "123456");
    java.sql.Statement st =  conn.createStatement();
} catch (SQLException e) {
    e.printStackTrace();
}

execute、executeQuery、executeUpdate

创建完 Statement,就要把 SQL 语句交给 Statement 对象去执行了。

executeQuery

一般如果是运行 查询(select) 语句,推荐使用 第一节中获得的conn 对象的 executeQuery 函数,executeQuery只能返回单个结果集,但是应对大部分的查询已经足够。

添加下面的代码到上面的 try 代码块中。

String sql = "select * from BeanBook";
st.executeQuery(sql);

executeUpdate

如果你需要运行 insertupdatedelete 等等语句,则可以使用 executeUpdate 函数,它不会返回结果集,但是会返回一个整数,代表受到影响的行数,如果是 0,代表了你没有改变任何数据库的内容,即调用失败了。

使用 executeUpdate :

String sql = "delete from BeanBook where price>50";
st.executeUpdate(sql);

execute

execute 是更加通用和强大的函数,但是它也比较复杂。它的返回值类型有很多。

execute 不仅可以做到 executeQuery 能做的事,也能做到 executeUpdate 能做到的事情。而且,它还能返回多个结果集。

正因为如此,它一般被用在一些执行未知 SQL 字符串的情况下,如果 SQL 语句能够确定,请尽可能不用 execute

它的返回值比较复杂,我们一般使用 getResultSetgetUpdateCount 获取,而不是直接把 st.execute(sql); 的结果拿来。

以下两段代码和上面两小节的 demo 的效果是一样的。

//代码段1
String sql = "select * from BeanBook";
st.execute(sql);
st.getResultSet();

//代码段2
String sql = "delete from BeanBook where price>50";
st.execute(sql);
st.getUpdateCount();

ResultSet

前面一直没提 ResultSet,ResultSet 就是 executeQuery 的返回值。

举个例子,使用 ResultSet 遍历 SQL 的结果:

String sql = "select * from BeanBook";
java.sql.ResultSet rs = st.executeQuery(sql);
while (rs.next()) {
    System.out.println(rs.getString(1) + "\t" + rs.getString(2) + "\t" + rs.getDouble(4));
}

ResultSet 通过 next 函数来遍历,next 从一条记录跳转到下一条记录。

getStringgetDouble 等等函数接受一个数字 n 作为参数,获得当前记录的第 n 个属性的值,并对这个值进行转换。

比如 getString(1) 获取第一个属性,转换成 String 类型;getDouble(4) 获取第四个属性,转换成 Double 类型。

下面是一些 get 函数 (不全):

  • 原始类型相关
    getString、getBoolean、getByte、getShort、getInt、getLong、getFloat、getDouble
  • 日期相关
    getDate、getTime、getTimestamp

PreparedStatement

通过 conn.prepareStatement 可以来创建一个 PrepareStatement 对象 (conn 是一个 java.sql.Connection)。

但是这个函数必须要给出一个 SQL 语句作为参数。

这里也可以看出 PrepareStatement 与 Statement 的一个比较大的区别。Statement 可以一直被复用,但是 PrepareStatement 每执行一次 SQL,都要创建新的 PrepareStatement。

//Statement
java.sql.Statement st =  conn.createStatement();
Int limit = 50;
String sql = "delete from BeanBook where price>50";
st.executeUpdate(sql);
String sql2 = "select * from BeanBook";
st.executeQuery(sql2);

//PrepareStatement
String sql = "delete from BeanBook where price>50";
java.sql.PreparedStatement pst = conn.prepareStatement(sql);
st.executeUpdate();
String sql2 = "select * from BeanBook";
java.sql.PreparedStatement pst2 = conn.prepareStatement(sql2);
st.executeQuery();

PreparedStatement 和 Statement 一样是用来执行 SQL 语句的,但是 Statement 有很多问题。

字符串拼接问题

假如我们希望可以动态地设置 SQL 语句,比如,动态改变 where 从句的条件,在 Statement 中,我们需要这样写:

java.sql.Statement st =  conn.createStatement();
Int limit = 50;
String sql = "delete from BeanBook where price>"+limit;
st.executeUpdate(sql);

PrepareStatement 允许一种可读性非常好的参数设置语法:

String sql = "delete from BeanBook where price>?";
java.sql.PreparedStatement pst = conn.prepareStatement(sql);
pst.setInt(1, 50);
st.executeUpdate(sql);

这个 ? 语法可不止可读性好,还有一个更重要的是,它支持了 预编译,这在我们接下来提到的性能问题中会被具体讨论。

还需要提一下的是 setInt 等等的 set 相关的函数,前面已经提过 get 函数了,基本上把 get 改成 set 就行了。

性能问题

Statement 的想法是对象只需要创建一次,后续只要传入不同的 SQL 就行了。但是在面对重复都比较高的工作的时候,这可能并不是很好。

比如,我执行了一次 insert into Beanbook(barcode, bookname, pubid, price, state) values(1, 'Java', 3, 56, '在库')

现在我稍微变一下,要插入 insert into Beanbook(barcode, bookname, pubid, price, state) values(2, 'C++', 2, 34, '在库')

这两条命令几乎一样,但是我却要编译两次!!!

我们来看看 PrepareStaement,我们完全可以使用 ? 语法,创建一次模版,因为存在预编译机制,当我们第二次插入的时候节省了一次编译的开销。

也就是说,在可以使用 ? 语法替换的一系列 SQL 操作中,使用 PrepareStatement 将会节省一大笔开销。

SQL 注入式攻击

SQL 注入式攻击其实很简单,这是完全可以避免的,但是使用 Statement 的时候你要格外小心。

假设不怀好意的用户对你的数据库请求删除一些合法的东西,比如删除 bookname 等于 'Java' 的书,但是他传给你的字符串做了一些手脚:

//用户的数据
String name="'Java' OR price>0";

//你的代码
String sql = "delete from BeanBook where bookname="+name;

好了,你完了,因为字符串拼接的时候,后面的 OR price>0 没有被当作是 bookname 的一部分,而是被当成是 SQL 命令的一部分!!! 在这里,你的数据库已经被清空了。

但是使用 ? 语法你完全不用担心,因为 PrepareStatement 是预编译的,后面只会插入数据,插入的内容不会被当作是 SQL 命令。

close

在数据库的最后不能忘记,关闭连接。

在原先代码的基础上,在最后的 finally 语句块中加入 close() 函数。

public static void main(String[] args) throws SQLException {
    java.sql.Connection conn = java.sql.DriverManager.getConnection("jdbc:mysql://localhost:3306/booklib", "root",
            "123456");
    try {
        conn.setAutoCommit(false);
        String sql = "select * from BeanBook";
        java.sql.PreparedStatement pst = conn.prepareStatement(sql);
        pst.executeQuery();
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        if (conn != null)
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
    }
}

JDBC时间类型的处理

从之前的 get 函数中,我们看到了日期相关的 API。

JDBC 中一共有三种时间类型:

  • Date
  • Time
  • Timestamp

System.currentTimeMillis()

这个函数可以获得 系统当前的一个时间戳。

时间戳是 1970年1月1日0点0分 到现在的毫秒数。

设置Timestamp

数据库里面存的都是 Timestamp,一般建议存取都用 Timestamp。

设置当前的时间为 Timestamp:

pst.setTimestamp(5, new java.sql.Timestamp(System.currentTimeMillis()));

通过 Date类 来创建 Timestamp:

java.utl.Date date = new java.util.Date();
pst.setTimestamp(5, new java.sql.Timestamp(date.getTime()));

事实上,一般这里直接用字符串也能设置,但对于数据库存在性能问题,一般不建议这样做:

pst.setString(5, "2020-06-27 00:00:00");

取出Timestamp

因为从数据库直接取出的是 Timestamp,需要使用 SimpleDateFormat 来格式化,才能打印出我们可以识别的时间字符串。

Timestamp timestamp = rs.getTimestamp(5);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(timestamp));

事务控制

为什么需要事务

一个事务是一系列用户定义的数据库操作序列(CRUD)。它的目的是把数据库的多个操作合并抽象成一个操作。

事务的设计哲学: 要么都成功,要么都失败。这就是事务的原子性

事务是隔离的,并发执行的事务之间不互相干扰。

如何实现

事务的实现靠的是回滚机制。

当你做完一个操作的时候,都有日志文件记录下你修改的数据。如果你接下来的操作出现了问题,那么数据库就能根据日志文件,运行逆操作,回到原来的状态。

JDBC中的事务编程

可以在最开始使用 setAutoCommit(false) 来关闭自动提交。

所谓的自动提交就是,mysql 的 JDBC实现 默认是一旦运行了 execute 相关的那三个函数,就会自动运行 commit() 函数,以更新数据库。

然后在 try 语句的最后使用 commit() 提交。

最后不要忘记,异常处理,如果发生了异常,就要使用 rollback() 函数回滚,使前面的操作全部无效。

我们来看一个结合来前面所有知识的例子:

import java.sql.SQLException;

public class Test {
    static {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws SQLException {
        java.sql.Connection conn = java.sql.DriverManager.getConnection("jdbc:mysql://localhost:3306/booklib", "root",
                "123456");
        try {
            conn.setAutoCommit(false);
            String sql = "select * from BeanBook";
            java.sql.PreparedStatement pst = conn.prepareStatement(sql);
            java.sql.ResultSet rs = pst.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getString(1) + "\t" + rs.getString(2) + "\t" + rs.getDouble(4));
            }
            sql = "delete from BeanBook where price>50";
            pst = conn.prepareStatement(sql);
            conn.commit();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (conn != null)
                try {
                    conn.rollback();
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
        }
    }

}

mysql 中的存储引擎有: InnoDBMyISAM等等,但是事务控制只有 InnoDB 支持。


连接池

连接池的概念

之前每次我们使用数据库 CRUD 的时候,我们每次都需要新建一个连接 Connection。

创建连接和关闭连接的过程也是比较消耗时间的,当线程数量很大的时候,系统就会变得卡顿。

连接池就是为了解决这个问题。连接池的设计哲学是: 总是借,而不创建

我们在一开始先创建一定数量的连接 Connection,然后每次有请求连接的时候,就找空闲的连接分配过去。如果没有空闲,则需要等待。

实现连接池

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class ConnectionPool {
    int size;
    List<Connection> conns = new ArrayList<Connection>();

    public ConnectionPool(int size) {
        //构造器
        this.size = size;
        init();
    }

    public void init() {
        //初始化连接池
        try {
            // Class.forName("com.mysql.jdbc.Driver"); //mysql5
            Class.forName("com.mysql.cj.jdbc.Driver"); // mysql6+
            for (int i = 0; i < size; i++) {
                Connection conn = DriverManager
                        .getConnection("jdbc:mysql://127.0.0.1:3306/booklib", "root", "123456");
                conns.add(conn);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public synchronized Connection getConnection() {
        //获得一个连接
        while (conns.isEmpty()) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Connection conn = conns.remove(0);
        return conn;
    }

    public synchronized void returnConnection(Connection conn) {
        //返还一个连接
        conns.add(conn);
        this.notifyAll();
    }
    
}

使用开源连接池(以 C3P0 为例)

一些著名的开源连接池

  • DBCP
  • C3P0

C3P0连接池 的使用:

import java.beans.PropertyVetoException;
import java.sql.Connection;
import java.sql.SQLException;
import com.mchange.v2.c3p0.ComboPooledDataSource;

public class ConnectionPoo {
	private static final String url="jdbc:mysql://localhost:3306/booklib";
	private static final String user="root";
	private static final String password="123456";
	private static ComboPooledDataSource dataSource;

	static{
		try {
			dataSource = new ComboPooledDataSource();
			dataSource.setUser(user);
			dataSource.setPassword(password);
			dataSource.setJdbcUrl(url);
			//dataSource.setDriverClass("com.mysql.jdbc.Driver");
            dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
			dataSource.setInitialPoolSize(5);
			dataSource.setMinPoolSize(1);
			dataSource.setMaxPoolSize(10);
			dataSource.setMaxStatements(50);
			dataSource.setMaxIdleTime(60);
		} catch (PropertyVetoException e) {
			throw new RuntimeException(e);
		}
	}
	public static Connection getConnection() throws SQLException{
		return dataSource.getConnection();
	}
}

c3p0 v0.9.2版本 之后,从中分离了一个 mchange-commons-java 包,作为使用 c3p0 的辅助包。我们这里调用的就是辅助包。


OR映射

JavaBean

一个 JavaBean 对象需要满足的条件:

  • 提供一个默认的无参构造函数。
  • 需要被序列化并且实现 Serializable 接口。
  • 一系列可读写属性。
  • 一系列的 getter 或 setter 方法。

所有对 JavaBean 属性的访问都应当使用 getter 和 setter 方法。

JavaBean 是一个可复用的组件,把应用的业务逻辑和显示逻辑分离开,降低了开发的复杂程度和维护成本。

POJO (Plain Ordinary Java Object)

POJO 是纯粹的 JavaBean。

JavaBean除了满足上面的条件,没有规定你不能定义其他东西,就算你把一些业务的代码加入进来也没关系。

POJO 不允许有业务方法,也不能携带 Connection 之类的方法。

一个简单的 POJO 对象:

public class BeanPublisher {
	private String pubid;
	private String publisherName;
	private String address;
	public String getPubid() {
		return pubid;
	}
	public void setPubid(String pubid) {
		this.pubid = pubid;
	}
	
	public String getPublisherName() {
		return publisherName;
	}
	public void setPublisherName(String publisherName) {
		this.publisherName = publisherName;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}
}

EntityBean 与 OR映射

OR映射 是把数据库对象和编程语言中的对象映射在一起,他们拥有一样的属性。

EntityBean 一般用于ORM对象关系映射,一个实体映射成一张表。

它能执行很多自动化操作:

  • 创建一个 EntityBean 对象相当于创建一条记录
  • 删除一个 EntityBean 对象会同时从数据库中删除对应记录
  • 修改一个 EntityBean 时,容器会自动将 EntityBean 的状态和数据库同步

一些流行的 ORM 框架:

  • Hibernate
  • MyBati
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章