1. 简介

Spring Data 的核心价值在于屏蔽底层数据存储的差异,让业务代码更专注于逻辑本身,而不是被各种持久化技术的细节缠住。无论是 JPA、MongoDB 还是其他存储引擎,Spring Data 都提供了一套统一的抽象层。

本文将系统梳理 Spring Data、Spring Data JPA 和 Spring Data MongoDB 中最常用、最实用的注解。这些注解是日常开发的“利器”,掌握它们能让你的数据访问层写得更简洁、更高效,避免踩坑。

2. Spring Data 通用注解

2.1. @Transactional

这是控制事务的“开关”。当你需要精确控制某个方法的事务行为时,直接在方法或类上加这个注解即可。

@Transactional
void pay() {}
  • 类上使用:该类所有 public 方法都启用事务。
  • ⚠️ 方法级覆盖:如果类上已标注,但某个方法需要不同的事务配置(比如 propagationisolation),直接在该方法上重新标注即可,会覆盖类级别的设置。

这个注解的参数非常丰富,比如传播行为、隔离级别、超时时间等。具体配置细节可以参考官方文档或相关文章,这里不展开,毕竟读者都是老手了。

2.2. @NoRepositoryBean

这个注解解决了一个很实际的问题:如何创建一个只用来被继承的“基类”Repository,而不想让 Spring 容器真的去实例化它

想象一下,你希望所有 Repository 都有一个通用的查询方法,比如 Optional<T> findById(ID id)。你可能会创建一个父接口:

@NoRepositoryBean
interface MyUtilityRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
    Optional<T> findById(ID id);
}
  • @NoRepositoryBean 的作用就是告诉 Spring Data:别为这个接口生成代理 Bean,它只是个“模板”。
  • ✅ 子接口不受影响:当你创建具体的 Repository 时,Spring 会正常为其创建 Bean。
@Repository
interface PersonRepository extends MyUtilityRepository<Person, Long> {}

⚠️ 注意:findById 方法在 Spring Data JPA 2.0+ 已是 CrudRepository 的标准方法,这里仅作示例。但在实际项目中,你可能会定义 findActiveByXXX 这类通用方法,这时 @NoRepositoryBean 就非常实用。

2.3. @Param

在自定义查询中,给参数起个“名字”,让 JPQL 或原生 SQL 更清晰、更易维护。

@Query("FROM Person p WHERE p.name = :name")
Person findByName(@Param("name") String name);
  • ✅ 使用 :name 的语法来引用参数。
  • ✅ 代码可读性高,一眼就能看出 :name 对应的是哪个参数。
  • ❌ 避免使用 ?1?2 这种位置参数,一旦参数顺序改变,查询就可能出错,是典型的“脆弱代码”。

2.4. @Id

标记实体类中的主键字段。这是最基础也最重要的注解之一。

class Person {
    @Id
    Long id;
    // ...
}
  • 实现无关性:无论你底层用的是 JPA、MongoDB 还是其他存储,@Id 的语义都是一致的,这使得你的实体类可以更容易地在不同存储间迁移或复用。

2.5. @Transient

标记一个字段不需要被持久化。这个字段只存在于内存中,不会映射到数据库的任何列。

class Person {
    // ...
    @Transient
    int age; // 可能是根据 birthDate 计算出来的
    // ...
}
  • ✅ 用途广泛:比如存放临时计算结果、DTO 中的额外字段等。
  • ✅ 同样具有实现无关性,与 @Id 一样,是跨存储的通用约定。

2.6. @CreatedBy, @LastModifiedBy, @CreatedDate, @LastModifiedDate

这几个注解是实现数据审计(Auditing)的利器,可以自动填充创建人、修改人、创建时间、修改时间。

public class Person {
    // ...
    @CreatedBy
    User creator;
    
    @LastModifiedBy
    User modifier;
    
    @CreatedDate
    Date createdAt;
    
    @LastModifiedDate
    Date modifiedAt;
    // ...
}
  • ✅ 只需在实体上标注,Spring Data 会在保存/更新时自动填充。
  • ⚠️ 关键前提:要让 @CreatedBy@LastModifiedBy 生效,必须集成 Spring Security。Spring 会通过 SecurityContextHolder 获取当前认证的用户(Principal)。
  • @CreatedDate@LastModifiedDate 通常不需要额外依赖,会自动使用当前时间。

3. Spring Data JPA 注解

3.1. @Query

定义 JPA 查询的“终极武器”,支持 JPQL 和原生 SQL。

  • JPQL 示例

    @Query("SELECT COUNT(*) FROM Person p")
    long getPersonCount();
    
  • 命名参数示例(推荐):

    @Query("FROM Person p WHERE p.name = :name")
    Person findByName(@Param("name") String name);
    
  • 原生 SQL 示例

    @Query(value = "SELECT AVG(p.age) FROM person p", nativeQuery = true)
    int getAverageAge();
    
    • nativeQuery = true 表示使用数据库原生 SQL。
    • ✅ 注意原生 SQL 是针对数据库表名(person),而 JPQL 是针对实体名(Person)。

3.2. @Procedure

简单粗暴地调用数据库的存储过程(Stored Procedure)

  1. 第一步:在实体上声明存储过程(使用 JPA 原生注解):

    @NamedStoredProcedureQueries({ 
        @NamedStoredProcedureQuery(
            name = "count_by_name", 
            procedureName = "person.count_by_name", 
            parameters = { 
                @StoredProcedureParameter(
                    mode = ParameterMode.IN, 
                    name = "name", 
                    type = String.class),
                @StoredProcedureParameter(
                    mode = ParameterMode.OUT, 
                    name = "count", 
                    type = Long.class) 
            }
        ) 
    })
    class Person {}
    
    • name: 在代码中引用该过程的逻辑名称。
    • procedureName: 数据库中存储过程的真实名称。
    • parameters: 定义输入(IN)和输出(OUT)参数。
  2. 第二步:在 Repository 中调用

    @Procedure(name = "count_by_name")
    long getCountByName(@Param("name") String name);
    
    • ✅ Spring Data JPA 会自动处理 IN/OUT 参数的映射,非常方便。

3.3. @Lock

为查询方法指定锁模式,用于处理并发场景。

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Person p WHERE p.id = :id")
Optional<Person> findByIdForUpdate(@Param("id") Long id);
  • ✅ 常用场景:在更新前锁定某条记录,防止并发修改。
  • ✅ 可用模式包括:
    • PESSIMISTIC_READ / PESSIMISTIC_WRITE (悲观锁)
    • OPTIMISTIC (乐观锁)
    • NONE (无锁)

3.4. @Modifying

任何修改数据的操作(UPDATE, DELETE, INSERT)都必须加上这个注解,否则会报错。

@Modifying
@Query("UPDATE Person p SET p.name = :name WHERE p.id = :id")
void changeName(@Param("id") long id, @Param("name") String name);
  • @Modifying 告诉 Spring Data 这是一个修改操作,需要在事务中执行。
  • ⚠️ 默认情况下,@Modifying 操作会清除该实体在当前持久化上下文(Persistence Context)中的缓存。如果不想清除,可以设置 clearAutomatically = false

3.5. @EnableJpaRepositories

启动 JPA Repository 支持的“总开关”。

@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
class PersistenceJPAConfig {}
  • ✅ 必须配合 @Configuration 使用。
  • basePackages 指定 Spring 扫描 Repository 接口的包路径。
  • Spring Boot 用户注意:只要 classpath 下有 Spring Data JPA 依赖,这个注解会自动配置,通常无需手动添加。

4. Spring Data MongoDB 注解

4.1. @Document

标记一个类为 MongoDB 的文档(Document),相当于 JPA 中的 @Entity

@Document
class User {}
  • ✅ 可以指定集合(Collection)名称:
    @Document(collection = "user")
    class User {}
    
    • 如果不指定,MongoDB 会默认使用类名的小写形式作为集合名。

4.2. @Field

定义实体字段在 MongoDB 文档中的键名(Key),相当于 JPA 中的 @Column

@Document
class User {
    // ...
    @Field("email")
    String emailAddress;
    // ...
}
  • ✅ 这样,Java 字段 emailAddress 在 MongoDB 中存储为键 email
  • ✅ 非常适合处理命名规范不一致的情况,比如 Java 用 camelCase,而数据库用 snake_case。

4.3. @Query

在 MongoDB Repository 中定义自定义查询,使用的是 MongoDB 的 JSON 查询语法。

@Query("{ 'name' : ?0 }")
List<User> findUsersByName(String name);
  • ?0 表示第一个方法参数,?1 表示第二个,以此类推。
  • ✅ 也可以使用命名参数:
    @Query("{ 'name' : :#{#name} }")
    List<User> findUsersByName(@Param("name") String name);
    

4.4. @EnableMongoRepositories

启动 MongoDB Repository 支持的“总开关”。

@Configuration
@EnableMongoRepositories(basePackages = "com.example.mongo.repository")
class MongoConfig {}
  • ✅ 用法和 @EnableJpaRepositories 几乎完全一致。
  • Spring Boot 用户注意:只要 classpath 下有 Spring Data MongoDB 依赖,会自动配置,通常无需手动添加。

5. 总结

本文系统地梳理了 Spring Data 生态中最核心的注解,覆盖了通用、JPA 和 MongoDB 三大场景。

  • ✅ 掌握 @Transactional@Modifying@Query 是玩转数据访问的基础。
  • @NoRepositoryBean 和审计注解能显著提升代码的复用性和规范性。
  • ✅ JPA 的 @Procedure@Lock,以及 MongoDB 的 @Document@Field,都是处理特定场景的“杀手锏”。

这些注解看似简单,但组合起来威力巨大。理解它们的原理和最佳实践,能让你在开发数据层时游刃有余,少走弯路。

示例代码已整理至 GitHub:


» 下一篇: Java每周,问题231