作为一名Java开发者,掌握SQL操作是必备技能。我见过太多项目因为SQL使用不当导致性能问题甚至安全漏洞。今天我就结合自己多年的实战经验,带大家系统梳理Java中常用的SQL操作。
我们先从最基础的建表开始。在MySQL中创建表时,合理的约束设计能保证数据完整性:
sql复制CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
student_no VARCHAR(20) UNIQUE NOT NULL,
name VARCHAR(50) NOT NULL,
gender CHAR(1) DEFAULT 'M' CHECK (gender IN ('M','F')),
age INT CHECK (age >= 10 AND age <= 50),
score DECIMAL(5,2),
class_id INT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (class_id) REFERENCES class(id)
ON DELETE SET NULL ON UPDATE CASCADE
);
这里有几个关键点需要注意:
提示:在Java项目中,建议使用Flyway或Liquibase管理数据库变更,而不是直接执行SQL脚本。
Java中执行SQL通常使用JDBC,虽然现在有MyBatis等ORM框架,但理解原生JDBC仍然很重要。下面是四大基础操作的对照表:
| 操作类型 | SQL示例 | JDBC关键代码 | 注意事项 |
|---|---|---|---|
| 插入 | INSERT INTO student(name,age) VALUES(?,?) |
PreparedStatement.setString(1,name) |
必须使用参数化查询防注入 |
| 查询 | SELECT * FROM student WHERE id=? |
ResultSet rs = stmt.executeQuery() |
避免使用SELECT * |
| 更新 | UPDATE student SET score=? WHERE id=? |
stmt.setDouble(1,score) |
必须带WHERE条件 |
| 删除 | DELETE FROM student WHERE id=? |
stmt.setInt(1,id) |
生产环境建议逻辑删除 |
实际开发中,简单查询往往不能满足需求。以下是几个实用技巧:
分页查询:
sql复制-- MySQL分页
SELECT * FROM student
ORDER BY score DESC
LIMIT 10 OFFSET 20; -- 获取第3页,每页10条
-- Java实现
String sql = "SELECT * FROM student LIMIT ? OFFSET ?";
stmt.setInt(1, pageSize);
stmt.setInt(2, (pageNum-1)*pageSize);
模糊查询:
sql复制-- 包含"张"的名字
SELECT * FROM student
WHERE name LIKE '%张%';
-- Java安全设置
stmt.setString(1, "%" + keyword + "%");
注意:LIKE '%关键词%'会导致全表扫描,大数据量时应考虑全文索引。
数据库事务是保证数据一致性的关键。JDBC中这样实现:
java复制Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false); // 开启事务
// 执行多个SQL操作
updateAccount(conn, fromId, -amount);
updateAccount(conn, toId, amount);
conn.commit(); // 提交事务
} catch (SQLException e) {
if(conn != null) conn.rollback(); // 回滚
throw e;
} finally {
if(conn != null) conn.close();
}
sql复制-- 好的索引使用
SELECT id,name FROM student
WHERE age > 20
ORDER BY score;
-- 需要建立(age,score)复合索引
单条SQL执行效率低时,考虑批量操作:
java复制// 批量插入
String sql = "INSERT INTO student(name,age) VALUES(?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
for(Student s : students) {
stmt.setString(1, s.getName());
stmt.setInt(2, s.getAge());
stmt.addBatch(); // 添加到批处理
}
int[] counts = stmt.executeBatch(); // 执行批处理
生产环境必须使用连接池,推荐配置:
properties复制# HikariCP配置示例
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
错误示范:
java复制String sql = "SELECT * FROM user WHERE name='" + name + "'";
// 如果name输入 ' OR '1'='1 就会导致注入
正确做法:
java复制String sql = "SELECT * FROM user WHERE name=?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, name);
典型症状:应用运行一段时间后无法获取数据库连接。
检查方法:
java复制// 推荐写法
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
// 执行操作
}
java复制public interface StudentRepository extends JpaRepository<Student, Long> {
// 方法名自动生成查询
List<Student> findByNameContaining(String name);
// 自定义查询
@Query("SELECT s FROM Student s WHERE s.score > :minScore")
List<Student> findByScore(@Param("minScore") double minScore);
}
xml复制<select id="findStudents" resultType="Student">
SELECT * FROM student
<where>
<if test="name != null">
AND name LIKE CONCAT('%',#{name},'%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
</where>
ORDER BY id
</select>
java复制@Service
@Transactional
public class StudentService {
@Transactional(readOnly = true)
public Student getById(Long id) {
// 查询操作
}
public void updateScore(Long id, Double score) {
// 更新操作
}
}
在实际项目中,我建议:
最后分享一个性能优化的小技巧:对于统计类查询,可以考虑使用数据库的物化视图(Materialized View)或者Redis缓存结果,特别是数据变化不频繁的场景。我曾经通过这个优化将报表查询从5秒降到了50毫秒。