@Entity 里面的 JPA 注解

JPA 协议中关于 Entity 的相关规定

我们先看一下 JPA 协议里面关于实体做了哪些规定。(这里推荐一个查看 JPA 协议的官方地址:https://download.oracle.com/otn-pub/jcp/persistence-2_2-mrel-spec/JavaPersistence.pdf

  1. 实体是直接进行数据库持久化操作的领域对象(即一个简单的 POJO,可以按照业务领域划分),必须通过 @Entity 注解进行标示。
  2. 实体必须有一个 public 或者 protected 的无参数构造方法。
  3. 持久化映射的注解可以标示在 Entity 的字段 field 上,如下所示:
@Column(length = 20, nullable = false)

private String userName;

除此之外,也可以将持久化注解运用在 Entity 里面的 get/set 方法上,通常我们是放在 get 方法中,如下所示:

@Column(length = 20, nullable = false)

public String getUserName(){

    return userName;

}

概括起来,就是 Entity 里面的注解生效只有两种方式:将注解写在字段上或者将注解写在方法上(JPA 里面称 Property)。

但是需要注意的是,在同一个 Entity 里面只能有一种方式生效,也就是说,注解要么全部写在 field 上面,要么就全部写在 Property 上面,因为我经常会看到有的同事分别在两种方式中加了注解后说:“哎呀,我的注解怎么没有生效呀!”因此这一点需要特别注意。

  1. 只要是在 @Entity 的实体里面被注解标注的字段,都会被映射到数据库中,除了使用 @Transient 注解的字段之外。
  2. 实体里面必须要有一个主键,主键标示的字段可以是单个字段,也可以是复合主键字段。

以上我只挑选了最关键的几条进行了介绍,如果你有兴趣可以读一读 Java Persistence API 协议,这样我们在做 JPA 开发的时候就会顺手很多,可以理解很多 Hibernate 里面实现方法。

这也为你提供了一条解决疑难杂症的思路,也就是当我们遇到解决不了的问题时,就去看协议、阅读官方文档,深入挖掘一下,可能就会找到答案。那么接下来我们看看实例里面常用的注解有哪些。

详细的注解都有哪些?

我们先通过源码看看 JPA 里面支持的注解有哪些。

首先,我们利用 IEDA 工具,打开 @Entity 所在的包,就可以看到 JPA 里面支持的注解有哪些。如下所示:

@Entity 里面的 JPA 注解

我们可以看到,在 jakarta.persistence-api 的包路径下面大概有一百多个注解,你在没事的时候可以到这里面一个一个地看,也可以到 JPA 的协议里面对照查看文档。

我在这里只提及一些最常见的,包括 @Entity、@Table、@Access、@Id、@GeneratedValue、@Enumerated、@Basic、@Column、@Transient、@Lob、@Temporal 等。

  1. @Entity 用于定义对象将会成为被 JPA 管理的实体,必填,将字段映射到指定的数据库表中,使用起来很简单,直接用在实体类上面即可,通过源码表达的语法如下:
@Target(TYPE) //表示此注解只能用在class上面

public @interface Entity {

   //可选,默认是实体类的名字,整个应用里面全局唯一。

   String name() default "";

}
  1. @Table 用于指定数据库的表名,表示此实体对应的数据库里面的表名,非必填,默认表名和 entity 名字一样。
@Target(TYPE) //一样只能用在类上面

public @interface Table {

   //表的名字,可选。如果不填写,系统认为好实体的名字一样为表名。

   String name() default "";

   //此表所在schema,可选

   String schema() default "";

   //唯一性约束,在创建表的时候有用,表创建之后后面就不需要了。

   UniqueConstraint[] uniqueConstraints() default { };

   //索引,在创建表的时候使用,表创建之后后面就不需要了。

   Index[] indexes() default {};

}
  1. @Access 用于指定 entity 里面的注解是写在字段上面,还是 get/set 方法上面生效,非必填。在默认不填写的情况下,当实体里面的第一个注解出现在字段上或者 get/set 方法上面,就以第一次出现的方式为准;也就是说,一个实体里面的注解既有用在 field 上面,又有用在 properties 上面的时候,看下面的代码你就会明白。
@Id

private Long id;

@Column(length = 20, nullable = false)

public String getUserName(){

    return userName;

}

那么由于 @Id 是实体里面第一个出现的注解,并且作用在字段上面,所以所有写在 get/set 方法上面的注解就会失效。而 @Access 可以干预默认值,指定是在 fileds 上面生效还是在 properties 上面生效。我们通过源码看下语法:

@Target( { TYPE, METHOD, FIELD })//表示此注解可以运用在class上(那么这个时候就可以指定此实体的默认注解生效策略了),也可以用在方法上或者字段上(表示可以独立设置某一个字段或者方法的生效策略);

@Retention(RUNTIME)

public @interface Access {

//指定是字段上面生效还是方法上面生效

    AccessType value();

}

public enum AccessType {

    FIELD,

    PROPERTY

}
  1. @Id 定义属性为数据库的主键,一个实体里面必须有一个主键,但不一定是这个注解,可以和 @GeneratedValue 配合使用或成对出现。
  2. @GeneratedValue 主键生成策略,如下所示:
public @interface GeneratedValue {

    //Id的生成策略

    GenerationType strategy() default AUTO;

    //通过Sequences生成Id,常见的是Orcale数据库ID生成规则,这个时候需要配合@SequenceGenerator使用

    String generator() default "";

}

其中,GenerationType 一共有以下四个值:

public enum GenerationType {

    //通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。

    TABLE,

    //通过序列产生主键,通过 @SequenceGenerator 注解指定序列名, MySql 不支持这种方式;

    SEQUENCE,

    //采用数据库ID自增长, 一般用于mysql数据库

    IDENTITY,

//JPA 自动选择合适的策略,是默认选项;

    AUTO

}
  1. @Enumerated 这个注解很好用,因为它对 enum 提供了下标和 name 两种方式,用法直接映射在 enum 枚举类型的字段上。请看下面源码。
@Target({METHOD, FIELD}) //作用在方法和字段上

public @interface Enumerated {

//枚举映射的类型,默认是ORDINAL(即枚举字段的下标)。

    EnumType value() default ORDINAL;

}

public enum EnumType {

    //映射枚举字段的下标

    ORDINAL,

    //映射枚举的Name

    STRING

}

再来看一个 User 里面关于性别枚举的例子,你就会知道 @Enumerated 在这里没什么作用了,如下所示:

//有一个枚举类,用户的性别

public enum Gender {

    MAIL("男性"), FMAIL("女性");

    private String value;

    private Gender(String value) {

        this.value = value;

    }

}

//实体类@Enumerated的写法如下

@Entity

@Table(name = "tb_user")

public class User implements Serializable {

    @Enumerated(EnumType.STRING)

    @Column(name = "user_gender")

    private Gender gender;

    .......................

}

这时候插入两条数据,数据库里面的值会变成 MAIL/FMAIL,而不是“男性” / 女性。

经验分享:  如果我们用 @Enumerated(EnumType.ORDINAL),这时候数据库里面的值是 0、1。但是实际工作中,不建议用数字下标,因为枚举里面的属性值是会不断新增的,如果新增一个,位置变化了就惨了。并且 0、1、2 这种下标在数据库里面看着非常痛苦,时间长了就会一点也看不懂了。

  1. @Basic 表示属性是到数据库表的字段的映射。如果实体的字段上没有任何注解,默认即为 @Basic。也就是说默认所有的字段肯定是和数据库进行映射的,并且默认为 Eager 类型。
public @interface Basic {

    //可选,EAGER(默认):立即加载;LAZY:延迟加载。(LAZY主要应用在大字段上面)

    FetchType fetch() default EAGER;

    //可选。这个字段是否可以为null,默认是true。

    boolean optional() default true;

}
  1. @Transient 表示该属性并非一个到数据库表的字段的映射,表示非持久化属性。JPA 映射数据库的时候忽略它,与 @Basic 有相反的作用。也就是每个字段上面 @Transient 和 @Basic 必须二选一,而什么都不指定的话,默认是 @Basic。
  2. @Column 定义该属性对应数据库中的列名。
public @interface Column {

    //数据库中的表的列名;可选,如果不填写认为字段名和实体属性名一样。

    String name() default "";

    //是否唯一。默认flase,可选。

    boolean unique() default false;

    //数据字段是否允许空。可选,默认true。

    boolean nullable() default true;

    //执行insert操作的时候是否包含此字段,默认,true,可选。

    boolean insertable() default true;

    //执行update的时候是否包含此字段,默认,true,可选。

    boolean updatable() default true;

    //表示该字段在数据库中的实际类型。

    String columnDefinition() default "";

   //数据库字段的长度,可选,默认255

    int length() default 255;

}
  1. @Temporal 用来设置 Date 类型的属性映射到对应精度的字段,存在以下三种情况:
  • @Temporal(TemporalType.DATE)映射为日期 // date (只有日期
  • @Temporal(TemporalType.TIME)映射为日期 // time (只有时间
  • @Temporal(TemporalType.TIMESTAMP)映射为日期 // date time (日期+时间

我们看一个完整的例子,感受一下上面提到的注解的完整用法,如下:

package com.example.jpa.example1;

import lombok.Data;

import javax.persistence.*;

import java.util.Date;

@Entity

@Table(name = "user_topic")

@Access(AccessType.FIELD)

@Data

public class UserTopic {

   @Id

   @Column(name = "id", nullable = false)

   @GeneratedValue(strategy = GenerationType.IDENTITY)

   private Integer id;

   @Column(name = "title", nullable = true, length = 200)

   private String title;

   @Basic

   @Column(name = "create_user_id", nullable = true)

   private Integer createUserId;

   @Basic(fetch = FetchType.LAZY)

   @Column(name = "content", nullable = true, length = -1)

   @Lob

   private String content;

   @Basic(fetch = FetchType.LAZY)

   @Column(name = "image", nullable = true)

   @Lob

   private byte[] image;

   @Basic

   @Column(name = "create_time", nullable = true)

   @Temporal(TemporalType.TIMESTAMP)

   private Date createTime;

   @Basic

   @Column(name = "create_date", nullable = true)

   @Temporal(TemporalType.DATE)

   private Date createDate;

   @Enumerated(EnumType.STRING)

   @Column(name = "topic_type")

   private Type type;

   @Transient

   private String transientSimple;

   //非数据库映射字段,业务类型的字段

   public String getTransientSimple() {

      return title + "auto:jack" + type;

   }

   //有一个枚举类,主题的类型

   public enum Type {

      EN("英文"), CN("中文");

      private final String des;

      Type(String des) {

         this.des = des;

      }

   }

}

细心的同学就会发现,我们在一开始的 demo 里面没有这么多注解呀,其实这里面的很多注解都可以省略,直接使用默认的就可以。如 @Basic、@Column 名字有一定的映射策略(我们在第 17 课时讲 DataSource 的时候会详细讲解映射策略),所以可以省略。

此外,@Access 也可以省略,我们只要在这些类里面保持一致就可以了。可能你会有疑问了,这么多注解都要手动一个一个配置吗?老师介绍一种简单的做法——利用工具去生成 Entity 类,将会节省很多时间。

上一篇:11.1 LAMP架构介绍;MySQL安装(上中下)


下一篇:Java 11 新特性,Java程序员必备