跳转至

Statement 与 CRUD

本文你会学到

  • executeQueryexecuteUpdateexecute 三种执行方法的区别
  • 通过 Statement 完成基本的查询、插入、删除操作
  • SQL 注入风险的成因和利用方式
  • 如何通过 RETURN_GENERATED_KEYS 获取自增主键

⚙️ 三种执行方法

执行 SQL 用哪个方法?——三种方式对比

方法 返回值 适用场景
executeQuery(sql) ResultSet 仅用于 SELECT 查询
executeUpdate(sql) int(受影响行数) INSERT / UPDATE / DELETE / DDL
execute(sql) booleantrue 表示有 ResultSet) 不确定 SQL 类型时的通用方法

初始化测试数据

建表并插入初始数据(@BeforeEach)
    /**
     * 每个测试前:建表并插入初始数据
     */
    @BeforeEach
    void setUp() throws SQLException {
        conn = DriverManager.getConnection(URL);
        try (Statement stmt = conn.createStatement()) {
            // 创建 users 表
            stmt.execute("CREATE TABLE IF NOT EXISTS users ("
                    + "id INT, "
                    + "name VARCHAR(50), "
                    + "age INT)");
            // 插入 3 条初始数据
            stmt.executeUpdate("INSERT INTO users VALUES (1, '张三', 25)");
            stmt.executeUpdate("INSERT INTO users VALUES (2, '李四', 30)");
            stmt.executeUpdate("INSERT INTO users VALUES (3, '王五', 28)");
        }
    }

📝 CRUD 操作实践

如何查询数据?——executeQuery

executeQuery 查询所有用户
    /**
     * 演示 executeQuery 查询所有用户
     */
    @Test
    void testExecuteQuery() throws SQLException {
        try (Statement stmt = conn.createStatement();
             // executeQuery 专用于 SELECT 语句,返回 ResultSet
             ResultSet rs = stmt.executeQuery("SELECT * FROM users ORDER BY id")) {

            int count = 0;
            while (rs.next()) {
                int id = rs.getInt("id");
                String name = rs.getString("name");
                int age = rs.getInt("age");
                System.out.printf("用户: id=%d, name=%s, age=%d%n", id, name, age);
                count++;
            }
            assertEquals(3, count, "应查询到 3 条用户记录");
        }
    }

如何插入数据?——executeUpdate

executeUpdate 执行 INSERT
    /**
     * 演示 executeUpdate 插入一条记录
     */
    @Test
    void testExecuteUpdateInsert() throws SQLException {
        try (Statement stmt = conn.createStatement()) {
            // executeUpdate 用于 INSERT/UPDATE/DELETE,返回受影响的行数
            int rowsAffected = stmt.executeUpdate(
                    "INSERT INTO users VALUES (4, '赵六', 35)");
            assertEquals(1, rowsAffected, "插入操作应影响 1 行");
            System.out.println("插入影响行数: " + rowsAffected);

            // 验证插入成功
            ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM users");
            rs.next();
            assertEquals(4, rs.getInt(1), "插入后应有 4 条记录");
        }
    }

如何删除数据?——executeUpdate

executeUpdate 执行 DELETE
    /**
     * 演示 executeUpdate 删除记录
     */
    @Test
    void testExecuteUpdateDelete() throws SQLException {
        try (Statement stmt = conn.createStatement()) {
            // 删除 age > 27 的用户(李四30岁、王五28岁,共 2 条)
            int rowsAffected = stmt.executeUpdate(
                    "DELETE FROM users WHERE age > 27");
            assertEquals(2, rowsAffected, "删除操作应影响 2 行");
            System.out.println("删除影响行数: " + rowsAffected);

            // 验证只剩 1 条记录
            ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM users");
            rs.next();
            assertEquals(1, rs.getInt(1), "删除后应只剩 1 条记录");
        }
    }

不确定 SQL 类型怎么办?——execute 通用方法

execute() 处理 SELECT 和 UPDATE 两种情况
    /**
     * 演示 execute() 方法,处理返回 ResultSet 或更新计数两种情况
     */
    @Test
    void testExecuteGeneric() throws SQLException {
        try (Statement stmt = conn.createStatement()) {
            // execute() 可以执行任意 SQL,返回 boolean:
            // true  -> 结果是 ResultSet(用 getResultSet() 获取)
            // false -> 结果是更新计数(用 getUpdateCount() 获取)

            // 情况1:执行 SELECT,返回 true
            boolean isResultSet = stmt.execute("SELECT * FROM users");
            assertTrue(isResultSet, "SELECT 语句应返回 true(有 ResultSet)");
            try (ResultSet rs = stmt.getResultSet()) {
                int count = 0;
                while (rs.next()) count++;
                assertEquals(3, count, "应查询到 3 条记录");
                System.out.println("execute SELECT 返回 ResultSet,记录数: " + count);
            }

            // 情况2:执行 UPDATE,返回 false
            boolean isResultSet2 = stmt.execute("UPDATE users SET age = age + 1 WHERE id = 1");
            assertFalse(isResultSet2, "UPDATE 语句应返回 false(无 ResultSet)");
            int updateCount = stmt.getUpdateCount();
            assertEquals(1, updateCount, "更新操作应影响 1 行");
            System.out.println("execute UPDATE 返回更新计数: " + updateCount);
        }
    }

🛡️ 安全与高级特性

SQL 注入风险演示

SQL 注入危险

使用 Statement 拼接用户输入会导致严重的安全漏洞。攻击者可通过精心构造的输入绕过认证、读取全表数据,甚至删除数据库。

SQL 注入风险演示——直接拼接用户输入
    /**
     * 演示 SQL 注入风险:用字符串拼接构造恶意查询
     * 这就是为什么需要 PreparedStatement 的原因!
     */
    @Test
    void testSqlInjectionDemo() throws SQLException {
        try (Statement stmt = conn.createStatement()) {
            // 模拟用户输入(恶意输入)
            String maliciousInput = "' OR '1'='1";

            // ⚠️ 危险:直接拼接用户输入到 SQL 中
            String sql = "SELECT * FROM users WHERE name = '" + maliciousInput + "'";
            System.out.println("拼接后的 SQL: " + sql);
            // 实际执行的 SQL: SELECT * FROM users WHERE name = '' OR '1'='1'
            // 条件 '1'='1' 恒为 true,导致查出所有记录

            ResultSet rs = stmt.executeQuery(sql);
            int count = 0;
            while (rs.next()) {
                count++;
            }
            // 注入成功:本应查不到数据,却查到了所有 3 条记录
            assertEquals(3, count, "SQL 注入导致查到了所有记录(应该 0 条,实际 3 条)");
            System.out.println("⚠️ SQL 注入成功!查到了 " + count + " 条记录(本不应查到任何数据)");
            // 这就是为什么必须使用 PreparedStatement 来防止 SQL 注入
        }
    }

插入后怎么拿到自增 ID?

INSERT 语句执行后,经常需要立即获取新记录的自增主键 id(例如插入订单后需要 orderId 用于后续关联)。JDBC 通过 Statement.RETURN_GENERATED_KEYS 标志和 getGeneratedKeys() 方法实现此功能。

executeUpdate 传入 RETURN_GENERATED_KEYS,插入后读取自增主键
    /**
     * 演示 Statement.RETURN_GENERATED_KEYS:插入后获取自动生成的主键值
     * 适用场景:INSERT 后需要立刻拿到新记录的自增 id,再用于关联操作
     */
    @Test
    void testGetGeneratedKeys() throws SQLException {
        // 建一张带自增主键的表用于演示
        try (Statement stmt = conn.createStatement()) {
            stmt.execute("CREATE TABLE IF NOT EXISTS orders ("
                    + "id INT AUTO_INCREMENT PRIMARY KEY, "
                    + "product VARCHAR(100), "
                    + "amount DOUBLE)");
        }

        // 关键:executeUpdate 第二个参数传 Statement.RETURN_GENERATED_KEYS
        // 告诉驱动:执行完 INSERT 后请保留生成的主键,供 getGeneratedKeys() 读取
        String sql = "INSERT INTO orders (product, amount) VALUES ('Java Book', 99.9)";
        try (Statement stmt = conn.createStatement()) {
            try {
                int rowsAffected = stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
                assertEquals(1, rowsAffected, "应插入 1 条记录");

                // 通过 getGeneratedKeys() 获取自动生成的主键
                try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
                    assertTrue(generatedKeys.next(), "应有生成的主键");
                    long generatedId = generatedKeys.getLong(1);
                    assertTrue(generatedId > 0, "生成的主键 id 应大于 0");
                    System.out.println("新插入记录的自增主键 id: " + generatedId);
                }
            } finally {
                // 使用 finally 确保断言失败时也能清理临时表
                stmt.execute("DROP TABLE IF EXISTS orders");
            }
        }
    }

PreparedStatement 同样支持

PreparedStatement 也支持获取自增主键,在 prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) 时传入标志即可,用法完全一致。