数据访问
Spring Boot 支持多种数据访问方案,从轻量的 JDBC 到全自动 ORM,按复杂度和灵活性分层选择。
方案全景
SQL 控制程度
高 ◄────────────────────────────────────────► 低
│ │
JdbcTemplate MyBatis Spring Data JPA Spring Data JDBC
(手写全部 SQL)(XML/注解)(自动生成 SQL) (轻量 ORM)
| 方案 | SQL 控制 | 学习曲线 | 复杂查询 | 推荐场景 |
|---|---|---|---|---|
| JdbcTemplate | 完全手写 | 低 | 灵活 | 简单 CRUD、遗留系统 |
| MyBatis | 手写 SQL + 映射 | 中 | 优秀 | 复杂查询、DBA 协作 |
| MyBatis-Plus | 单表自动、复杂手写 | 低 | 优秀 | 业务开发首选 |
| Spring Data JPA | 自动生成 + JPQL | 高 | 一般 | 领域驱动设计、标准化 CRUD |
| Spring Data JDBC | 手写 SQL + 简单映射 | 低 | 一般 | 轻量 ORM 替代 |
JdbcTemplate
Spring 对 JDBC 的薄封装,自动处理连接获取/释放、异常转换。
@Repository
@RequiredArgsConstructor
public class UserJdbcRepository {
private final JdbcTemplate jdbc;
// 查询列表
public List<User> findByStatus(int status) {
return jdbc.query(
"SELECT id, name, email FROM users WHERE status = ?",
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
),
status
);
}
// 查询单个(结果为空时抛 EmptyResultDataAccessException)
public Optional<User> findById(Long id) {
try {
User user = jdbc.queryForObject(
"SELECT * FROM users WHERE id = ?",
BeanPropertyRowMapper.newInstance(User.class),
id
);
return Optional.ofNullable(user);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
// 插入并返回自增主键
public Long insert(User user) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbc.update(conn -> {
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO users (name, email) VALUES (?, ?)",
Statement.RETURN_GENERATED_KEYS
);
ps.setString(1, user.getName());
ps.setString(2, user.getEmail());
return ps;
}, keyHolder);
return keyHolder.getKey().longValue();
}
// 批量插入
public void batchInsert(List<User> users) {
jdbc.batchUpdate(
"INSERT INTO users (name, email) VALUES (?, ?)",
users,
500,
(ps, user) -> {
ps.setString(1, user.getName());
ps.setString(2, user.getEmail());
}
);
}
// NamedParameterJdbcTemplate:命名参数(可读性更好)
public List<User> findByIds(List<Long> ids) {
String sql = "SELECT * FROM users WHERE id IN (:ids)";
MapSqlParameterSource params = new MapSqlParameterSource("ids", ids);
return namedJdbc.query(sql, params,
BeanPropertyRowMapper.newInstance(User.class));
}
}Spring Data JPA
基于 Hibernate,通过接口声明自动生成 SQL,适合标准 CRUD 和领域建模。
// 实体
@Entity
@Table(name = "orders")
@Data
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String orderNo;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
// Repository 接口:Spring Data 自动实现
public interface OrderRepository extends JpaRepository<Long, Order> {
// 方法名推导 SQL
List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);
// @Query 写 JPQL(面向实体,非表名)
@Query("SELECT o FROM Order o WHERE o.createdAt > :since AND o.status = :status")
List<Order> findRecentByStatus(
@Param("since") LocalDateTime since,
@Param("status") OrderStatus status
);
// 原生 SQL
@Query(
value = "SELECT * FROM orders WHERE amount > :amount LIMIT :limit",
nativeQuery = true
)
List<Order> findHighValueOrders(@Param("amount") BigDecimal amount,
@Param("limit") int limit);
// 分页
Page<Order> findByUserId(Long userId, Pageable pageable);
// 更新(需 @Modifying + @Transactional)
@Modifying
@Transactional
@Query("UPDATE Order o SET o.status = :status WHERE o.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") OrderStatus status);
// Projection:只查需要的字段
List<OrderSummary> findByStatus(OrderStatus status);
}
// Projection 接口
public interface OrderSummary {
Long getId();
String getOrderNo();
OrderStatus getStatus();
}分页使用:
Pageable pageable = PageRequest.of(0, 20,
Sort.by(Sort.Direction.DESC, "createdAt"));
Page<Order> page = orderRepo.findByUserId(userId, pageable);
page.getContent(); // 当前页数据
page.getTotalElements(); // 总记录数
page.getTotalPages(); // 总页数JPA 详细配置见 JPA与Hibernate。
MyBatis
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true # user_name → userName
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发打印 SQL// Mapper 接口
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(Long id);
// 复杂查询用 XML
List<User> findByCondition(UserQueryParam param);
@Insert("INSERT INTO users(name, email) VALUES(#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
}<!-- mapper/UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="UserResultMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="name"/>
<collection property="roles" ofType="Role"
select="selectRolesByUserId" column="id"/>
</resultMap>
<select id="findByCondition" resultMap="UserResultMap">
SELECT * FROM users
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="ids != null and ids.size() > 0">
AND id IN
<foreach collection="ids" item="id"
open="(" separator="," close=")">#{id}
</foreach>
</if>
</where>
ORDER BY created_at DESC
LIMIT #{offset}, #{pageSize}
</select>
</mapper>MyBatis-Plus 的代码生成与单表 CRUD 详见 MyBatis-Plus,MyBatis 完整配置见 MyBatis。
Spring Data 通用能力
无论 JPA 还是 MongoDB 等,Spring Data 都提供统一的抽象:
// 通用审计(自动填充创建人/更新时间)
@Configuration
@EnableJpaAuditing
public class JpaConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext()
.getAuthentication())
.map(Authentication::getName);
}
}
@Entity
public class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
// Specification(动态条件,JPA)
public class OrderSpec {
public static Specification<Order> hasStatus(OrderStatus status) {
return (root, query, cb) ->
status == null ? null : cb.equal(root.get("status"), status);
}
public static Specification<Order> createdAfter(LocalDateTime time) {
return (root, query, cb) ->
time == null ? null : cb.greaterThan(root.get("createdAt"), time);
}
}
// 使用
List<Order> orders = orderRepo.findAll(
Specification.where(OrderSpec.hasStatus(PAID))
.and(OrderSpec.createdAfter(LocalDate.now().atStartOfDay()))
);数据访问层选型建议
- 新项目 + 标准 CRUD 多:MyBatis-Plus(单表自动,复杂 SQL 手写)
- 领域驱动设计 + 复杂关联:Spring Data JPA + Hibernate
- 报表 / 数仓 / 复杂 SQL 多:MyBatis(XML 管理 SQL,DBA 可维护)
- 微服务 + 极简依赖:Spring Data JDBC 或 JdbcTemplate
相关链接
- JPA与Hibernate — 实体生命周期、懒加载、N+1 问题
- MyBatis — XML 动态 SQL、拦截器、插件
- MyBatis-Plus — 代码生成、条件构造器、逻辑删除
- 事务管理 —
@Transactional与数据访问层的边界 - 多数据源 — 多个 DataSource 的路由配置
- 读写分离 — 主从路由与延迟问题
- 连接池配置 — HikariCP / Druid 参数调优
- 数据库迁移 — Flyway / Liquibase Schema 版本管理