MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java数据库编程的基本概念

2021-01-157.1k 阅读

Java 数据库编程概述

在当今的软件开发领域,数据存储与管理是几乎所有应用程序不可或缺的部分。Java 作为一种广泛使用的编程语言,提供了丰富的工具和技术来与各种数据库进行交互。Java 数据库编程涉及到如何使用 Java 代码来访问、操作和管理数据库中的数据。这包括连接数据库、执行 SQL 语句、处理结果集以及处理事务等操作。

数据库驱动程序

数据库驱动程序是 Java 程序与数据库之间的桥梁。它负责将 Java 代码中的数据库操作请求转换为数据库能够理解的命令,并将数据库的响应转换为 Java 程序能够处理的格式。不同类型的数据库需要相应的驱动程序,例如 MySQL 数据库需要 MySQL Connector/J 驱动,Oracle 数据库需要 Oracle JDBC 驱动等。

加载驱动程序

在 Java 程序中使用数据库驱动,首先需要加载驱动程序类。在早期,通常使用 Class.forName() 方法来加载驱动程序,例如对于 MySQL 驱动:

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

从 JDBC 4.0 开始,大多数 JDBC 驱动程序支持自动加载,即不需要显式调用 Class.forName() 方法。但在一些旧版本的应用或特定场景下,显式加载驱动程序依然是必要的。

数据库连接

建立数据库连接是进行数据库编程的第一步。Java 提供了 java.sql.Connection 接口来表示数据库连接。要获取一个连接对象,需要使用 DriverManager 类的 getConnection() 方法。该方法通常需要传入数据库的 URL、用户名和密码。

数据库 URL

数据库 URL 是用于标识数据库位置和类型的字符串。不同数据库的 URL 格式有所不同。例如,MySQL 数据库的 URL 格式通常为:jdbc:mysql://主机名:端口号/数据库名,如果使用默认端口 3306,且主机名为本地主机,可以简化为 jdbc:mysql://localhost/数据库名。对于 Oracle 数据库,URL 格式通常为 jdbc:oracle:thin:@主机名:端口号:服务名

获取连接示例

以下是获取 MySQL 数据库连接的示例代码:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseConnectionExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password)) {
            if (connection != null) {
                System.out.println("成功连接到数据库");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,使用 DriverManager.getConnection() 方法尝试获取数据库连接。try-with-resources 语句确保在使用完连接后,无论是否发生异常,都会自动关闭连接,从而避免资源泄漏。

SQL 语句执行

一旦建立了数据库连接,就可以执行 SQL 语句来对数据库进行操作。Java 提供了 StatementPreparedStatementCallableStatement 接口来执行 SQL 语句。

Statement 接口

Statement 接口是执行 SQL 语句的基本接口。它用于执行静态 SQL 语句,即 SQL 语句在编写代码时就已经确定,不会根据程序运行时的变量而改变。

执行查询语句

执行查询语句使用 executeQuery() 方法,该方法返回一个 ResultSet 对象,包含查询结果。以下是一个查询示例:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;

public class StatementQueryExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password);
             Statement statement = connection.createStatement();
             ResultSet resultSet = statement.executeQuery("SELECT * FROM users")) {
            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                System.out.println("ID: " + id + ", Name: " + name);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,首先创建了一个 Statement 对象,然后使用 executeQuery() 方法执行 SQL 查询语句。通过 ResultSet 对象的 next() 方法遍历结果集,并使用 getInt()getString() 方法获取相应列的值。

执行更新语句

执行更新语句(如 INSERTUPDATEDELETE)使用 executeUpdate() 方法,该方法返回一个整数,表示受影响的行数。示例如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.sql.SQLException;

public class StatementUpdateExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password);
             Statement statement = connection.createStatement()) {
            int rowsAffected = statement.executeUpdate("INSERT INTO users (name) VALUES ('John')");
            System.out.println("插入的行数: " + rowsAffected);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,使用 executeUpdate() 方法执行 INSERT 语句,并输出受影响的行数。

PreparedStatement 接口

PreparedStatement 接口继承自 Statement 接口,用于执行预编译的 SQL 语句。预编译的 SQL 语句在数据库服务器端进行编译,提高了执行效率,并且可以有效防止 SQL 注入攻击。

参数化查询

PreparedStatement 使用占位符(?)来代替实际参数。在执行语句前,通过 setXXX() 方法为占位符设置具体的值。以下是一个参数化查询示例:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class PreparedStatementQueryExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password);
             PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM users WHERE name =?")) {
            preparedStatement.setString(1, "John");
            try (ResultSet resultSet = preparedStatement.executeQuery()) {
                while (resultSet.next()) {
                    int id = resultSet.getInt("id");
                    String name = resultSet.getString("name");
                    System.out.println("ID: " + id + ", Name: " + name);
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,PreparedStatement 的 SQL 语句使用 ? 作为占位符,然后通过 setString() 方法为第一个占位符设置值。

执行更新操作

PreparedStatement 执行更新操作同样使用 executeUpdate() 方法。示例如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class PreparedStatementUpdateExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password);
             PreparedStatement preparedStatement = connection.prepareStatement("UPDATE users SET name =? WHERE id =?")) {
            preparedStatement.setString(1, "Jane");
            preparedStatement.setInt(2, 1);
            int rowsAffected = preparedStatement.executeUpdate();
            System.out.println("更新的行数: " + rowsAffected);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,通过 PreparedStatement 执行 UPDATE 语句,设置了新的用户名和对应的用户 ID。

CallableStatement 接口

CallableStatement 接口用于执行数据库存储过程。存储过程是预编译并存储在数据库中的一组 SQL 语句,可以接受参数并返回结果。

调用不带参数的存储过程

假设数据库中有一个不带参数的存储过程 getAllUsers,以下是调用该存储过程的示例:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class CallableStatementExample1 {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password);
             CallableStatement callableStatement = connection.prepareCall("{call getAllUsers()}")) {
            try (ResultSet resultSet = callableStatement.executeQuery()) {
                while (resultSet.next()) {
                    int id = resultSet.getInt("id");
                    String name = resultSet.getString("name");
                    System.out.println("ID: " + id + ", Name: " + name);
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,使用 prepareCall() 方法创建 CallableStatement 对象,并传入调用存储过程的 SQL 语句。然后执行存储过程并处理结果集。

调用带参数的存储过程

如果存储过程 getUserById 接受一个参数 id,示例代码如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class CallableStatementExample2 {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password);
             CallableStatement callableStatement = connection.prepareCall("{call getUserById(?)}")) {
            callableStatement.setInt(1, 1);
            try (ResultSet resultSet = callableStatement.executeQuery()) {
                if (resultSet.next()) {
                    int id = resultSet.getInt("id");
                    String name = resultSet.getString("name");
                    System.out.println("ID: " + id + ", Name: " + name);
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,通过 setInt() 方法为存储过程的参数设置值,然后执行存储过程并处理结果。

结果集处理

ResultSet 接口用于表示数据库查询的结果集。它提供了一系列方法来遍历结果集并获取其中的数据。

遍历结果集

通常使用 while 循环和 next() 方法来遍历结果集。next() 方法将光标移动到下一行,如果结果集中还有下一行,则返回 true,否则返回 false。示例如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;

public class ResultSetTraversalExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password);
             Statement statement = connection.createStatement();
             ResultSet resultSet = statement.executeQuery("SELECT * FROM users")) {
            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                System.out.println("ID: " + id + ", Name: " + name);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,通过 while (resultSet.next()) 循环遍历结果集,并获取每一行的 idname 列的值。

获取列值

ResultSet 提供了多种方法来获取不同类型列的值,如 getInt()getString()getDate() 等。列值可以通过列名或列索引来获取。使用列名获取值更加直观和安全,因为即使列的顺序发生变化,代码依然可以正确获取值。例如:

int id = resultSet.getInt("id");
String name = resultSet.getString("name");

如果使用列索引获取值,索引从 1 开始。例如:

int id = resultSet.getInt(1);
String name = resultSet.getString(2);

结果集的类型和并发模式

ResultSet 有不同的类型和并发模式。类型包括 TYPE_FORWARD_ONLY(默认,只能向前遍历结果集)、TYPE_SCROLL_INSENSITIVE(可以前后滚动结果集,且对其他事务对数据库的修改不敏感)和 TYPE_SCROLL_SENSITIVE(可以前后滚动结果集,且对其他事务对数据库的修改敏感)。并发模式包括 CONCUR_READ_ONLY(只读,不能更新结果集)和 CONCUR_UPDATABLE(可更新结果集)。

要创建具有特定类型和并发模式的 ResultSet,需要在创建 StatementPreparedStatement 时指定。例如:

Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);

事务处理

事务是一组数据库操作的集合,这些操作要么全部成功执行,要么全部不执行。在 Java 数据库编程中,事务处理通过 Connection 接口的方法来实现。

开启事务

默认情况下,JDBC 连接是自动提交模式,即每执行一条 SQL 语句就立即提交到数据库。要开启事务,需要关闭自动提交模式,示例如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class TransactionExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password)) {
            connection.setAutoCommit(false);

            try (PreparedStatement preparedStatement1 = connection.prepareStatement("INSERT INTO accounts (account_name, balance) VALUES ('Account1', 1000)");
                 PreparedStatement preparedStatement2 = connection.prepareStatement("INSERT INTO transactions (transaction_type, amount) VALUES ('Deposit', 1000)")) {
                preparedStatement1.executeUpdate();
                preparedStatement2.executeUpdate();
                connection.commit();
            } catch (SQLException e) {
                connection.rollback();
                e.printStackTrace();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,通过 connection.setAutoCommit(false) 关闭自动提交模式,开启事务。

提交事务

当所有数据库操作都成功完成后,需要调用 commit() 方法提交事务,将所有操作永久保存到数据库。例如:

connection.commit();

回滚事务

如果在事务执行过程中发生错误,需要调用 rollback() 方法回滚事务,撤销所有未提交的操作。例如:

connection.rollback();

数据库元数据

数据库元数据提供了关于数据库结构和特性的信息。Java 提供了 DatabaseMetaDataResultSetMetaData 接口来获取数据库元数据。

DatabaseMetaData

DatabaseMetaData 接口提供了关于数据库的整体信息,如数据库产品名称、版本、支持的 SQL 语法等。可以通过 Connection 对象的 getMetaData() 方法获取 DatabaseMetaData 对象。示例如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;

public class DatabaseMetaDataExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password)) {
            DatabaseMetaData metaData = connection.getMetaData();
            System.out.println("数据库产品名称: " + metaData.getDatabaseProductName());
            System.out.println("数据库版本: " + metaData.getDatabaseProductVersion());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,获取了数据库的产品名称和版本信息。

ResultSetMetaData

ResultSetMetaData 接口提供了关于 ResultSet 结构的信息,如列数、列名、列类型等。可以通过 ResultSet 对象的 getMetaData() 方法获取 ResultSetMetaData 对象。示例如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.sql.SQLException;

public class ResultSetMetaDataExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password);
             Statement statement = connection.createStatement();
             ResultSet resultSet = statement.executeQuery("SELECT * FROM users")) {
            ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            System.out.println("列数: " + columnCount);

            for (int i = 1; i <= columnCount; i++) {
                System.out.println("列名: " + metaData.getColumnName(i) + ", 列类型: " + metaData.getColumnTypeName(i));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,获取了结果集的列数、列名和列类型信息。

连接池

在实际应用中,频繁地创建和销毁数据库连接会消耗大量的系统资源,降低应用程序的性能。连接池技术通过预先创建一定数量的数据库连接,并将这些连接保存在池中,当应用程序需要数据库连接时,从池中获取连接,使用完毕后再将连接归还到池中,从而提高连接的复用率,减少资源消耗。

常见的连接池实现

Java 中有多种连接池实现,如 Apache Commons DBCP、HikariCP 等。以 HikariCP 为例,使用步骤如下:

首先,在项目的 pom.xml 文件中添加 HikariCP 的依赖:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>4.0.3</version>
</dependency>

然后,在代码中使用 HikariCP 获取数据库连接:

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.SQLException;

public class HikariCPExample {
    public static void main(String[] args) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("root");
        config.setPassword("password");

        HikariDataSource dataSource = new HikariDataSource(config);

        try (Connection connection = dataSource.getConnection()) {
            if (connection != null) {
                System.out.println("成功从连接池获取连接");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,首先创建了一个 HikariConfig 对象并设置数据库连接相关的属性,然后使用该配置创建 HikariDataSource 对象。通过 HikariDataSourcegetConnection() 方法从连接池中获取数据库连接。

异常处理

在 Java 数据库编程中,可能会抛出各种 SQLException。合理的异常处理对于保证程序的稳定性和可靠性至关重要。

捕获和处理 SQLException

通常在 try-catch 块中捕获 SQLException,并根据具体情况进行处理。例如:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class SQLExceptionHandlingExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password);
             PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO users (name) VALUES (?)")) {
            preparedStatement.setString(1, "Invalid' OR '1'='1");
            preparedStatement.executeUpdate();
        } catch (SQLException e) {
            System.out.println("数据库操作出错: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

在上述代码中,如果执行 INSERT 语句时发生 SQLException,会捕获并打印错误信息。这里故意设置一个可能导致 SQL 注入的字符串,以展示异常处理情况。

异常链

SQLException 可以包含一个原因异常,形成异常链。通过 getCause() 方法可以获取原因异常,以便进行更深入的错误分析。例如:

try {
    // 数据库操作代码
} catch (SQLException e) {
    Throwable cause = e.getCause();
    if (cause != null) {
        System.out.println("原因异常: " + cause.getMessage());
    }
    e.printStackTrace();
}

总结

Java 数据库编程涵盖了从数据库连接建立、SQL 语句执行、结果集处理到事务管理等多个方面。合理使用数据库驱动、连接池,正确处理 SQL 语句和异常,对于开发高效、稳定的数据库应用程序至关重要。通过不断学习和实践,开发者可以熟练掌握 Java 数据库编程技术,为各种业务场景提供可靠的数据存储和管理解决方案。同时,随着数据库技术的不断发展,如 NoSQL 数据库的兴起,Java 也提供了相应的技术和工具来与这些新型数据库进行交互,开发者需要持续关注并学习新的知识和技能,以适应不断变化的开发需求。