JDBC

JDBC

Java数据库连接(Java Database Connectivity)是用来规范客户端如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。通过JDBC,可以使用Java通过数据库驱动连接到数据库,编写SQL语句进行操作。

JDBC实现了开放式数据库连接(Open Database Connectivity)标准——一种早期使用C语言实现的数据库连接标准。ODBC提供了一套用来访问数据库的标准API,同时提供了驱动管理器,当数据库驱动注册到驱动管理器后,能够通过该驱动连接特定的数据库,对数据库进行访问。而JDBC就是Java版本的ODBC。


数据库驱动主要由数据库的提供者开发。对于JDBC,不同的数据库驱动能够操作相对应的数据库。JDBC的驱动类型主要分为以下4种:

  • JDBC-ODBC桥:此类驱动程序将JDBC调用转换成ODBC调用 , 然后使用ODBC与数据库进行通信。早期Java包含了驱动程序:sun.jdbc.odbc.JdbcOdbcDriver,但使用之前需要对ODBC数据源进行配置。JDK1.8不再提供JDBC/ODBC桥的支持。
  • 本地API驱动:用来和数据库的客户端API进行通信。使用前,需要安装Java类库和一些平台相关的代码(特定的驱动程序,类似ODBC)。通过驱动程序的转换,把Java程序中使用的JDBC API转换成NativeAPI,进而存取数据库。
  • 网络协议驱动:使用与具体数据库无关的协议,将数据库请求发送给服务器中间件。服务器再将数据库请求翻译成符合数据库规范的调用,再把这种调用传给数据库服务器。执行效率受网络带宽的影响。
  • 本地协议驱动:将JDBC请求直接转换成符合相关数据库系统规范的请求,可以直接和数据库通信。这种类型的驱动完全由Java实现,因此实现了平台独立性。执行效率相对于其他三种类型,不需要将jdbc调用转换成odbc/本地数据库接口/中间层服务器,效率高。最常用的驱动方式。

连接步骤

1. 注册驱动
2. 获取连接对象
3. 获取Statement对象
4. 执行SQL语句
5. 处理查询结果集
6. 关闭连接

注册驱动

因为JDBC就是Java版本的ODBC,和ODBC一样,也需要将数据库驱动注册到驱动管理器后才能和数据库进行通信

  • java.sql.Driver是JDBC中提供的驱动接口,每一种数据库的驱动类都要实现这个接口。
    • 对于Oracle,驱动可以使用oracle.jdbc.driver.OracleDriver或oracle.jdbc.OracleDriver,前者是后者的子类。它们都是java.sql.Driver的实现类。
  • 在底层,当加载了一个驱动类后便能够将驱动注册到驱动管理器中。
  • 加载驱动类的方法包括:创建驱动对象、Class.forName(驱动类全类名)、设置环境变量,共三种方法。
DriverManager.registerDriver(
    new oracle.jdbc.driver.OracleDriver()); // 在启动管理器注册驱动对象
new oracle.jdbc.driver.OracleDriver(); // 当驱动对象创建时也会自动注册

Class.forName("oracle.jdbc.driver.OracleDriver"); // 使用反射可以加载驱动类,从而注册驱动对象,最推荐

System.setProperty("jdbc.drivers", "oracle.jdbc.driver.OracleDriver");
// 设置System环境变量,将jdbc.drivers的值设置为驱动类的全类名

获取连接对象

java.sql.DriverManager是JDBC的驱动管理器,注册驱动后,可通过它获取到数据库连接对象。

  • 告诉驱动管理器要连接到哪个数据库,管理器就会使用该数据库的驱动,并返回一个连接对象。该操作通过DriverManager.getConnection(url,user,password)方法实现。
  • 要连接的数据库通过url确定,并通过用户名和密码连接到数据库。
String url = "jdbc:oracle:thin:@localhost:1521:XE";
// 格式:jdbc/odbc:数据库厂商:连接方式(thin/fat):@ip:端口:数据库名
String user = "jy"; // 用户名
String password = "jy"; // 密码
Connection connection = DriverManager.getConnection(url, user, password);

Properties properties = new Properties(); // 也可以使用Properties对象
properties.setProperty("user", "jy");
properties.setProperty("password", "jy");
Connection connection = DriverManager.getConnection(url, properties);

执行SQL语句并处理

通过连接对象,可以向数据库传输SQL语句,对数据库进行操作。如果是查询操作,还会返回一个查询结果。

  • SQL语句使用Statement进行包装。使用connection.createStatement()方法获取SQL语句对象。语句对象可以执行一条SQL,也可以批量执行。
  • 返回的查询结果使用java.sql.ResultSet进行包装。ResultSet的数据按数据类型和字段进行存储,不同的数据类型可以使用不同的方法获取。使用迭代器获取每一行的数据。
  • 获取结果集时,列索引从1开始;列名为查询时指定的列名(别名)。
Statement statement = connection.createStatement();
String sql = "create table "
    + "kk(id number primary key,"
    + "name varchar2(50))"; // 这里的SQL语句不能有分号
statement.execute(sql); // 执行SQL,返回一个boolean,表示有没有查询结果
statement.executeUpdate(sql); // 执行SQL,获取影响的行数,查询不影响行数

ResultSet result = statement.executeQuery(sql); // 执行SQL,获取查询结果集
while (set.next()) { // 有下一个记录时
    long id = set.getLong(1); // 获取第1列,是Long类型的
    String name = set.getString(2); // 获取第2列,是String类型的
    long id2 = set.getLong("id"); // 获取Long类型的id列
    String name2 = set.getString("name"); // 获取String类型的name列
    System.out.println(id + ": " + name);
}

String sql2 = "delete from kk where id = 1";
String sql3 = "update kk set name = '12222' where id = 2";
statement.addBatch(sql); // 添加语句
statement.addBatch(sql2);
statement.addBatch(sql3);
int[] is = statement.executeBatch(); // 批量执行SQL语句,返回的数组表示每条SQL影响的行数

关闭连接

数据库操作完毕后,使用close()方法关闭ResultSet、Statement和Connection连接,释放资源。

注意事项

配置文件

在前两个步骤中,因为不同的应用程序要连接的数据库是不一样的,故需要指定不同的驱动,设置不同的url、用户名和密码。这就需要对该步骤进行封装,并从外部文件获取数据库配置。这也是为什么推荐使用反射的forName()方法来注册驱动的原因。数据库配置可以从Properties配置文件中读取。
在对项目进行打包后,配置文件的路径可以从类加载路径获取。当配置文件放在src目录下时,配置文件的路径就是类加载器的根路径:

InputStream is = Test.class.getClassLoader().getResourceAsStream("datasource.properties");
properties.load(is);

Statement

除了普通的SQL语句接口Statement,该接口还有两个子接口:PreparedStatement和CallableStatement。其中后者又是前者的子接口。
PreparedStatement(简称PS)是预处理的SQL语句接口,它具有以下特殊功能:

  • 预处理:能够对SQL进行预处理,在处理过程中将语句传递给数据库进行验证,避免错误的SQL。同时可以在此过程中对SQL语句进行检查,避免SQL语句注入等安全问题。
  • 占位符:PS具有占位符功能,对SQL检查完毕后,此后的数据只需填补占位符(?)即可,不用发送完整的SQL语句,避免连接字符串。对于大量重构的SQL语句(即结构相同数据不同的语句),速度更快。注意,表名等元数据不能使用占位符,只有数据可以。
  • 批处理:PS只能批处理同构的SQL语句。
String sql = "insert into t_user(id, name, age) "
    + "values(user_seq.nextval, ?, ?)";
ps = conn.prepareStatement(sql);
for (int i = 1; i <= 10000; i++) {
    ps.setString(1, "tom" + i); // 1是占位符序号,后面的是数据
    ps.setInt(2, 20);
    ps.executeUpdate(); // 执行
    // ps.addBatch(); 添加到批处理
}
// ps.executeBatch(); 执行批处理

CallableStatement能够调用数据库中的存储过程。

事务处理

JDBC默认自动提交事务,但这样破坏了事务的原子性。可以改为手动提交。也可以回滚和设置记录点。

connection.setAutoCommit(false); // 设置不自动提交事务
connection.commit(); // 提交一次事务
connection.rollback(); // 回滚事务
Savepoint savepointC = connection.setSavepoint("c"); // 设置记录点
connection.rollback(savepointC); // 回滚到记录点

数据库连接池

数据库连接池

数据库连接对象的个数是有限的。采用数据库连接池可以复用数据库连接,提高并发访问的速度、可靠性和安全性,不必反复创建和销毁数据库连接。常用的数据库连接池有Druid、HikariCP、Tomcat-jdbc、DBCP、C3PO,其中Druid是阿里巴巴开发的开源数据库连接池,在性能、监控、诊断、安全、扩展性等方面远超其它几个连接池。


javax.sql.DataSource是Java中数据连接池的接口,可以通过它获取到的数据库连接对象。对连接池使用close()方法不会再关闭连接,而是归还连接到连接池中。Druid连接池com.alibaba.druid.pool.DruidDataSource就是DataSource接口的实现。以下是一个简单的获取连接池代码:

DataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(properties.getProperty("jdbc.driver"));
dataSource.setUrl(properties.getProperty("jdbc.url"));
dataSource.setUsername(properties.getProperty("jdbc.user"));
dataSource.setPassword(properties.getProperty("jdbc.password"));
dataSource.setInitialSize(5); // 初始连接个数
dataSource.setMaxActive(10); // 最大连接个数
dataSource.setLoginTimeout(5); // 登录超时秒数
dataSource.setQueryTimeout(5); // 查询超时秒数
Connection connection = dataSource.getConnection();

如果想直接从配置文件读取配置构建连接池对象,则为:

Properties properties = new Properties();
properties.load(ConnectionPoolTest.class
    .getClassLoader().getResourceAsStream("druid.properties"));
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);

配置文件的键名是固定的:(规定在com.alibaba.druid.pool.DruidDataSourceFactory中)

driverClassName=oracle.jdbc.OracleDriver(驱动类名)
url=jdbc:oracle:thin:@localhost:1521:XE
username=jy
password=jy
initialSize=5(初始连接池个数)
maxActive=10(最大连接池个数)
maxWait=3000(最大等待时间)

封装

对JDBC的封装分为两个方面:对JDCB连接的过程进行封装、对从数据库获取的数据进行封装。

  • 对过程的封装可以分为四个部分:获取连接对象、通过查询语句获取查询结果集、执行非查询语句、关闭连接。
  • 对获取的数据进行封装,即将要数据表编成一个pojo类,将获取的数据转为对象,每条记录对应类的一个对象,再调用对象的toString方法对其进行输出。

其中要注意的地方有:

  • 当从数据库连接池获取连接对象时,由于数据库连接池只创建一个,故可以将连接池的创建放在静态代码块中。
  • 获取查询结果集时,有如下几个“境界”:
    • 传入一个SQL查询、对获取的结果集进行手动封装成数据对象列表:可以在获得SQL语句后,执行语句并返回ResultSet对象,由主方法对RS进行封装。这一过程可以简化为传入一个SQL语句,并实现一个函数式接口(Function)。接口的实现即是对数据的封装过程。
public static <T> List<T> queryForList(String sql, 
	Function<ResultSet, List<T>> function) throws Exception {
    Connection connection = getConnection(true); // 这里的true可以封装成常量
    Statement statement = connection.createStatement();
    ResultSet resultSet = statement.executeQuery(sql);
    List<T> list = function.apply(resultSet);
    close(connection, statement, resultSet);
    return list;
}
public static void main(String[] args) {
    // 获取连接略
    List<KK> data = DBUtils.queryForList(sql, t -> {
        try {
            List<KK> list = new ArrayList<>();
            while(t.next()) {
                KK kk = new KK();
                kk.setId(t.getInt("id"));
                kk.setName(t.getString("name"));
                kk.setAge(t.getInt("age"));
                kk.setBirthday(t.getDate("birthday"));
                list.add(kk);
            }
            return list;
        } catch (SQLException e) {
            return null;
        }
    });
    data.forEach(System.out::println);
}
  • 传入一个SQL查询、自动封装后返回数据对象列表:关键是自动封装的过程。首先需要传入pojo类的class对象。自动封装就需要获取当前数据的类型,并调用pojo类中当前数据的set方法。可以使用反射实现:①首先获取pojo类的属性列表,②遍历属性列表,获取属性名和属性类型,③根据属性类型确定调用的resultSet.getType系列方法和通过class.getDeclaredMethod获取的set方法的参数类型、根据属性名确定列名和set方法的名字。
/****** 查询方法中 ******/
while (resultSet.next()) {
    U instance = clazz.newInstance(); // 通过类的class对象得到pojo类对象
    List<TypeAndName> parse = TypeAndName.parse(clazz);
    // 这里封装了一个TypeAndName的类
    // 传入一个pojo类class对象,获取属性的类型和名字,并封装成TypeAndName对象存入一个列表
    for (TypeAndName typeAndName : parse) { // 遍历每个属性的类型和名字
        if ("int".equals(typeAndName.type)) { // 如果当前属性的类型是int
            typeAndName.invokeSet(instance, int.class, resultSet.getInt(typeAndName.name));
            // 通过当前属性的名字调用set方法,封装该属性
        } else if (typeAndName.type.contains("String")) {
            typeAndName.invokeSet(instance, String.class, resultSet.getString(typeAndName.name));
        }
    }
    list.add(instance); // 将封装好的pojo类对象放入列表
}
/***** parse方法(TypeAndName类中) ******/
// <T> List<TypeAndName> parse(Class<T> clazz)
Field[] fields = clazz.getDeclaredFields(); // 获取属性列表
List<TypeAndName> list = new ArrayList<>();
for (Field field : fields) {
    TypeAndName typeAndName = new TypeAndName(field.getType().getName(), field.getName());
    // 将当前属性的类型和名字封装成TypeAndName对象并存入一个列表
    list.add(typeAndName);
}
return list; // 将列表返回
/****** invokeSet方法(TypeAndName类中) ******/
// <T> void invokeSet(Object obj, Class<T> clazz, Object value)
String methodName = "set" + initCap(name); // 构造set方法名:set+属性名首字母大写
Method method = obj.getClass().getDeclaredMethod(methodName, clazz);
// 获取上面方法名、参数列表为属性类型的方法
method.invoke(obj, value); // 调用这个属性的set方法
  • 传入一个表名、自动返回数据对象列表:首先需要通过表名构造SQL语句,然后通过反射封装对象后返回。
  • 传入一个表名、自动生成这个表的类并返回该类的对象列表:~~这太变态了。~~连表里有什么样的数据、有几个数据、每个数据的列名(属性名)和类型都不知道。这需要通过获取表的元数据才能确定。然后通过列名和数据类型构造pojo类,并封装对象后返回。
  • 关闭连接时要注意进行判空处理。

元数据

可以通过连接对象获取数据库和数据库对象的元数据(MetaData),即定义数据的数据。其中java.sql.DatabaseMetaData是数据库的元数据,java.sql.ResultSetMetaData是数据库对象的元数据。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值