Hibernate JPA 注解分析

Hibernate JPA 注解分析

参考资料 & 鸣谢:

  1. JPA 批注参考:https://www.oracle.com/cn/middleware/technologies/ias/toplink-jpa-annotations.html
  2. JPA 批注参考:https://blog.51cto.com/u_9058648/3562214
  3. https://blog.csdn.net/yswKnight/article/details/79257372
  4. https://blog.csdn.net/yiyelanxin/article/details/100107335
  5. https://www.cnblogs.com/banywl/articles/14004262.html
  6. https://blog.csdn.net/justlpf/article/details/84956212

1、常用注解

Entity

@Entity 应用于实体类,并且使用默认的ORM规则,即 Class 名对应数据库表中表名,Class 字段名即表中的字段名(如想改变这种默认的ORM规则,就要使用 @Table 来改变 Class 名与数据库中表名的映射规则,@Column 来改变 Class 字段名与DB中表的字段名的映射规则)

元数据属性说明:

  • name(可选):表名

下面的代码说明,Customer类对应数据库中的Customer表,其中name为可选,缺省类名即表名!

@Entity(name="Customer")
public class Customer {}

Table

@Table用来定义Entity主表的name,catalog,schema等属性。

元数据属性说明:

  • name(可选):指定表的名称
  • catalog(可选):指定数据库名称(对应关系数据库中的catalog)
  • schema(可选):指定数据库的用户名(对应关系数据库中的schema)
  • UniqueConstraints(可选):指定唯一性字段约束的列,字段值唯一不重复(定义一个UniqueConstraint数组)
  • indexes(可选):索引,只有创建表的时候有用,默认不需要(indexes是一个数组)
@Entity(name="Customer")
@Table(name = "tb_customer", catalog = "db_jpa", schema = "root", 
       uniqueConstraints={@UniqueConstraint(columnNames={"name", "age"})
})
public class Customer {}

Entity 与 Table 对比

比较:@Entity(name = “实体名”) 、@Entity @Table(name = “表名”)
参考:https://mp.weixin.qq.com/s/GYMJT5sNoJ8-KcdIVQAoNw

我们在使用JPA的时候,需要做数据表和实体类的映射,@Entity注解的实体类,默认的实体名是非限定类名,对实体名首字母小写后得到表名,例如:

@Entity
public class Comment{}

实体名:Comment,表名:comment。但实际项目中,有的表名以 tb_ 开头,那就映射不到了,那么就需要把表名写出来,有两种方式可以写:

  1. @Entity(name = “实体名”),一般把实体名写成跟表名相同,相当于写出了表名
  2. @Entity + @Table(name = “表名”)

那么哪种方式好呢?粗略一看,应该是第一种好,但是应用到JPQL语句时,未必。

例如只用@Entity定义:

// 定义实体类
@Entity(name = "tb_comment")
public class Comment{}

// JPQL 语句
@Query("update tb_comment c set c.author = ?1 where c.id= ?2 ")

这是JPQL语句,JPA一不小心就把tb_comment认为是表名,后面的 c.xxx 就会去表中找字段,如果字段名跟实体的属性名不一样,就报错了。

如果写成下面这样的,使用@Entity + @Table(name = “表名”):

// 定义实体类
@Entity
@Table(name = "tb_comment")
public class Comment{}

// JPQL 语句
@Query("update Comment c set c.author = ?1 where c.id = ?2")

可读性好,Comment一看就是实体类名,后面的c.xxx就会去类中找属性名,但实体名不能用JPQL的保留字。

结论:

  1. @Entity(name = “实体名”):会降低JPQL语句的可读性,一般实体名不会跟JPQL保留字重复
  2. @Entity + @Table(name = “表名”):可读性强,更容易看出来面向对象编程思想,但要避免实体名与JPQL保留字重复

Column

基于Column注解的columnDefinition用法:https://www.jb51.net/article/226168.htm

@Column应用于实体类属性,可以指定数据库表字段的名字和其他属性。

@Target({METHOD, FIELD}) 
@Retention(RUNTIME)
public @interface Column {
    String name() default "";
    boolean unique() default false;
    boolean nullable() default true;
    boolean insertable() default true;
    boolean updatable() default true;
    String columnDefinition() default "";
    String table() default "";
    int length() default 255;
    int precision() default 0;
    int scale() default 0;
}

元数据属性说明:

  1. name:表示数据库表中该字段的名称,默认情形属性名称一致
  2. unique:表示该字段是否是唯一标识,默认:false
  3. nullable:表示该字段是否允许为null,默认:true
  4. insertable:表示在ORM框架执行插入操作时,该字段是否应出现INSETRT语句中,默认:true
  5. updateable:表示在ORM框架执行更新操作时,该字段是否应该出现在UPDATE语句中,默认:true。对于一经创建就不能更改的字段,该属性非常有用,比如email属性。(insertable、updatable属性一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的)
  6. columnDefinition:表示该字段在数据库中的实际类型。通常ORM框架可以根据属性类型自动判断数据库中字段的类型,但是依然有些例外:Date类型无法确定数据库中字段类型究竟是DATE、TIME、TIMESTAMP。此外,String的默认映射类型为VARCHAR,如果希望将String类型映射到特定数据库的BLOB或TEXT字段类型,则需要进行设置
  7. table:表示当映射多个表时,指定表的表中的字段。默认值为主表的表名
  8. length:表示该字段的大小,仅对String类型的字段有效
  9. precision:表示精度,当字段类型为double时,precision表示数值的总长度
  10. scale:表示精度,当字段类型为double时,scale表示小数点所占的位数

Column注解的columnDefinition用法:

columnDefinition属性表示创建表时,该字段创建的SQL语句,一般用于通过Entity生成表定义时使用,如果数据库中表已经建好,该属性没有必要使用

/**
 * 1、指定字段类型、长度、是否允许null、是否唯一、默认值
 */
@Column(name = "code",columnDefinition = "Varchar(100) not null default'' unique")
private String code;

/**
 * 2、需要特殊指定字段类型的情况
 * String默认映射类型为VARCHAR, 如果要将String类型映射到特定数据库的BLOB或TEXT字段类型
 */
@Column(name = "text",columnDefinition="TEXT")
private String text;
@Column(name = "salary", columnDefinition = "decimal(5,2)")
private BigDecimal salary;
@Column(name="birthday",columnDefinition="date")
private Date birthday;
@Column(name="createTime",columnDefinition="datetime")
private Date createTime;

Basic

应用于实体类属性,表明该字段是要映射到数据库表,@Entity标注的实体类,所有属性默认为@Basic(所以默认该注解可以不加)

元数据属性说明:

  • fetch:属性的读取策略,有两种策略:FetchType.LAZY(延迟加载)和FetchType.EAGER(立即加载),默认:EAGER
  • optional:表示该属性是否可以为null,默认值:true

PS 注意:@Basic(fetch =FetchType.LAZY) 标注某属性时,表示只有调用Hibernate对象的该属性的get方法时,才会从数据库表中查找对应该属性的字段值

Transient

@Transient作用在类属性上,与@Basic作用相反,表明该属性不需要持久化,JPA映射数据时忽略该属性

Temporal

@Temporal作用在类属性上,用来设置Date类型的属性映射到数据库时的精度。当我们使用到 java.util 包中的时间日期类型,则需要此注释来说明转化成 java.util包中的类型。

  1. @Temporal(TemporalType.DATE):映射为日期 date (只有日期)等于Java的java.sql.Date
  2. @Temporal(TemporalType.TIME):映射为日期 time (是有时间)等于Java的java.sql.Time
  3. @Temporal(TemporalType.TIMESTAMP):映射为日期 date + time (日期 + 时间)等于Java的java.sql.Timestamp

PS 注意:@Temporal注解只能映射java.util.Date or java.util.Calendar,这两种时间类型,否则将报如下错误:

Hibernate.AnnotationException: @Temporal should only be set on a java.util.Date or java.util.Calendar property: entity.User.createLocalDateTime

1、首先定义实体类,并给需要处理的时间字段加上@Temporal注解。示例代码:

@Data
@Entity
@Table(name = "sys_user")
public class User implements Serializable {
    @Id
    private Long id;
    @Temporal(TemporalType.DATE)
    private java.util.Date createDate;
    @Temporal(TemporalType.TIME)
    private java.util.Date createTime;
    @Temporal(TemporalType.TIMESTAMP)
    private java.util.Date createTIMESTAMP;
    // @Temporal只能映射 ava.util.Date or java.util.Calendar 两种时间类型
    // @Temporal(TemporalType.TIMESTAMP)
    // private java.time.LocalDateTime createLocalDateTime;
}

2、启动项目或者测试类,查看建表语句

create table sys_user (
    id bigint not null,
    createDate date,
    createTIMESTAMP timestamp,
    createTime time,
    primary key (id)
)

3、测试代码

import entity.User;
import org.junit.Test;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class TestDemo {

    @Test
    public void testSave() {
        // 1.加载配置文件创建工厂(实体管理器工厂)对象
        EntityManagerFactory factory = Persistence.createEntityManagerFactory("myJpa");
        // 2.通过实体管理器工厂获取实体管理器
        EntityManager em = factory.createEntityManager();
        // 3.获取事务对象,开启事务
        EntityTransaction tx = em.getTransaction();

        // 开启事物新增数据,手动设置时间
        tx.begin();
        User user = new User();
        user.setId(1L);
        user.setCreateDate(new java.util.Date());
        user.setCreateTime(new java.util.Date());
        user.setCreateTIMESTAMP(new java.util.Date());
        em.persist(user);
        tx.commit();

        em.refresh(user);
        User user_selelct = em.find(User.class, 1L);
        System.out.println(user_selelct);

        // 6.释放资源
        em.close();
        factory.close();
    }
}

4、查看输出日志

User(id=1, createDate=2022-02-01, createTime=15:13:36, createTIMESTAMP=2022-02-01 15:13:36.325)

CreationTimestamp

@CreationTimestamp 与 @UpdateTimestamp 注解是JPA用来自动创建、更新时间戳

使用该注解可以让Hibernate JPA在插入时针对注解的属性对应的日期类型创建默认值。此批注没有属性。

/**
 * Marks a property as the creation timestamp of the containing entity. The property value will be set to the current
 * VM date exactly once when saving the owning entity for the first time.
 * 

* Supported property types: *

    *
  • {@link java.util.Date}
  • *
  • {@link java.util.Calendar}
  • *
  • {@link java.sql.Date}
  • *
  • {@link java.sql.Time}
  • *
  • {@link java.sql.Timestamp}
  • *
  • {@link java.time.Instant}
  • *
  • {@link java.time.LocalDate}
  • *
  • {@link java.time.LocalDateTime}
  • *
  • {@link java.time.LocalTime}
  • *
  • {@link java.time.MonthDay}
  • *
  • {@link java.time.OffsetDateTime}
  • *
  • {@link java.time.OffsetTime}
  • *
  • {@link java.time.Year}
  • *
  • {@link java.time.YearMonth}
  • *
  • {@link java.time.ZonedDateTime}
  • *
* * @author Gunnar Morling */
@ValueGenerationType(generatedBy = CreationTimestampGeneration.class) @Retention(RetentionPolicy.RUNTIME) public @interface CreationTimestamp { }

示例代码参考@UpdateTimestamp

UpdateTimestamp

使用该注解可以让Hibernate JPA在更新时时针对注解的属性对应的日期类型创建默认值。此批注没有属性。

/**
 * Marks a property as the update timestamp of the containing entity. The property value will be set to the current VM
 * date whenever the owning entity is updated.
 * 

* Supported property types: *

    *
  • {@link java.util.Date}
  • *
  • {@link java.util.Calendar}
  • *
  • {@link java.sql.Date}
  • *
  • {@link java.sql.Time}
  • *
  • {@link java.sql.Timestamp}
  • *
  • {@link java.time.Instant}
  • *
  • {@link java.time.LocalDate}
  • *
  • {@link java.time.LocalDateTime}
  • *
  • {@link java.time.LocalTime}
  • *
  • {@link java.time.MonthDay}
  • *
  • {@link java.time.OffsetDateTime}
  • *
  • {@link java.time.OffsetTime}
  • *
  • {@link java.time.Year}
  • *
  • {@link java.time.YearMonth}
  • *
  • {@link java.time.ZonedDateTime}
  • *
* * @author Gunnar Morling */
@ValueGenerationType(generatedBy = UpdateTimestampGeneration.class) @Retention(RetentionPolicy.RUNTIME) public @interface UpdateTimestamp { }

1、一般情况下创建、更新时间戳注解 @CreationTimestamp、@UpdateTimestamp 会一起使用。下面是代码示例:

@Data
@Entity
@Table(name = "sys_user")
public class User implements Serializable {
    @Id
    private Long id;
    @CreationTimestamp
    private java.util.Date createDate;
    @UpdateTimestamp
    private java.sql.Timestamp updateTimestamp;
    @CreationTimestamp
    private java.time.LocalDate createLocalDate;
    @UpdateTimestamp
    private java.time.LocalTime updateLocalTime;
    @UpdateTimestamp
    private java.time.LocalDateTime updateLocalDateTime;
}

2、启动项目或者测试类,查看JPA自动建表语句。可以看到他们的对应关系,使用时需要特别注意。

create table sys_user (
    id bigint not null,
    createDate timestamp,
    createLocalDate date,
    updateLocalDateTime timestamp,
    updateLocalTime time,
    updateTimestamp timestamp,
    primary key (id)
)

3、运行单元测试,查看手动设置时间与JPA自动设置时间的区别

import entity.User;
import org.junit.Test;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Date;

public class TestDemo {

    @Test
    public void testSave() {
        // 1.加载配置文件创建工厂(实体管理器工厂)对象
        EntityManagerFactory factory = Persistence.createEntityManagerFactory("myJpa");
        // 2.通过实体管理器工厂获取实体管理器
        EntityManager em = factory.createEntityManager();
        // 3.获取事务对象,开启事务
        EntityTransaction tx = em.getTransaction();

        // 开启事物新增数据,手动设置时间
        tx.begin();
        User user = new User();
        user.setId(1L);
        user.setCreateDate(new Date());
        user.setCreateLocalDate(LocalDate.now());
        user.setUpdateTimestamp(new Timestamp(System.currentTimeMillis()));
        user.setUpdateLocalTime(LocalTime.now());
        user.setUpdateLocalDateTime(LocalDateTime.now());
        em.persist(user);
        tx.commit();

        em.refresh(user);
        User user_selelct = em.find(User.class, 1L);
        System.out.println(user_selelct);

        // 开启事物新增数据,JPA自动设置时间
        tx.begin();
        User user2 = new User();
        user2.setId(2L);
        em.persist(user2);
        tx.commit();

        em.refresh(user2);
        User user_selelct2 = em.find(User.class, 2L);
        System.out.println(user_selelct2);

        // 6.释放资源
        em.close();
        factory.close();
    }
}

4、查看输出日志

User(id=1, createDate=2022-01-31 21:59:16.181, updateTimestamp=2022-01-31 21:59:16.181, createLocalDate=2022-01-31, updateLocalTime=21:59:16, updateLocalDateTime=2022-01-31T21:59:16.181568)
User(id=2, createDate=2022-01-31 21:59:16.245, updateTimestamp=2022-01-31 21:59:16.245, createLocalDate=2022-01-31, updateLocalTime=21:59:16, updateLocalDateTime=2022-01-31T21:59:16.245574)

自定义默认值设置注解

参考文献 & 鸣谢:

  1. JPA注解@CreationTimestamp和@UpdateTimestamp:https://blog.csdn.net/z185665096/article/details/106090223
  2. JPA设置默认值、Timestamp设置、自动获取时间:https://blog.csdn.net/ctwy291314/article/details/88250205

比如,用户名,插入时可以根据当前线程直接获取,然后利用注解赋值,这样在业务处理时就可以不用关注该属性。类似还有更新时自动赋值操作人等。

  • 新增注解@CreationUser
/**
 * Hibernate新增插入时自动填充
 */
@ValueGenerationType(
    generatedBy = CreationUserGeneration.class
)
@Retention(RetentionPolicy.RUNTIME)
public @interface CreationUser {
}
  • 新增注解值生成类。主要继承org.hibernate.tuple.AnnotationValueGeneration
import org.hibernate.tuple.AnnotationValueGeneration;
import org.hibernate.tuple.GenerationTiming;
import org.hibernate.tuple.ValueGenerator;

/**
 * 针对加了注解{@link @CreationUser}的属性在插入时,自动根据当前线程获取用户信息赋值
 */
public class CreationUserGeneration implements AnnotationValueGeneration<CreationUser> {
    private ValueGenerator<?> generator;

    public CreationUserGeneration() {
    }

    @Override
    public void initialize(CreationUser annotation, Class<?> propertyType) {
        // 主要逻辑在这里实现
        this.generator = (ValueGenerator<Object>) (session, o) -> UserContextHolder.getUserName();
    }

    @Override
    public GenerationTiming getGenerationTiming() {
        // 只有插入时,每次都修改使用GenerationTiming.ALWAYS
        return GenerationTiming.INSERT;
    }

    @Override
    public ValueGenerator<?> getValueGenerator() {
        return this.generator;
    }

    @Override
    public boolean referenceColumnInSql() {
        return false;
    }

    @Override
    public String getDatabaseGeneratedReferencedColumnValue() {
        return null;
    }
}

就这样就OK了,注解@CreationUser就可以直接在实体类使用了。

2、主键注解

Id

@Id 定义了映射到数据库表的主键的属性,一个实体只能有一个属性被映射为主键,可置于getXxxx()前。

IdClass

当实体类使用复合主键时,需要定义一个类作为 ID 实体类。作为联合主键类,需要满足以下要求:

  1. 必须实现Serializable
  2. 必须有默认的public无参构造方法
  3. 必须覆盖equals和hashCode方法(EntityManager通过find方法查找Entity时是根据equals来判断的)

元数据属性说明:

  • value: id class的类名

假设user_article表中的联合主键是 title 与create_user_id,联合主键类代码如下:

@Data
public class UserArticleKey implements Serializable {
    private String title;
    private Long createUserId;
    public UserArticleKey() {
    }
    public UserArticleKey(String title, String content, Long createUserId) {
        this.title = title;
        this.createUserId = createUserId;
    }
}

user_article表实体类如下:

@Data
@Entity
@Table(name="user_article")
@IdClass(value = UserArticleKey.class)
public class UserArticle {
    private Integer id;
    @Id
    private String title;
    @Id
    private Long createUserId;
}

EmbeddedId

使用 @EmbeddedId 批注指定一个由实体拥有的可嵌入复合主键类(通常由两个或更多基元类型或 JDK 对象类型组成)。从原有数据库映射时(此时数据库键由多列组成),通常将出现复合主键。(或者也可以使复合主键类(IdClass)成为非嵌入类)

复合主键类具有下列特征:

  • 它是一个普通的旧式 Java 对象 (POJO) 类。

  • 它必须为 public,并且必须有一个 public 无参数构造函数。

  • 如果使用基于属性的访问,则主键类的属性必须为 public 或 protected。

  • 它必须是可序列化的。

  • 它必须定义 equals 和 hashCode 方法。

  • 这些方法的值相等性的语义必须与键映射到的数据库类型的数据库相等性一致。

  • 此批注没有属性,一般与 @Embeddable(注解在类上,表示此类是可以被其他类嵌套)配合使用。

  • @Embeddable 还能多层嵌套使用

假设sys_user表中的联合主键是 id 与 username,联合主键类代码如下:

1、主键的实体类

@Data
@Embeddable
public class UserPK implements Serializable {
    @Column(name = "id")
    private Long id;
    @Column(name = "username")
    private String username;
}

2、表映射的持久类(持久类里要有一个注解@EmbeddedId修饰主键的成员变量,其他的和普通的Entity类一样。它所对应复合主键类需要使用@Embeddable注解)

@Data
@Entity
@Table(name = "sys_user")
public class User implements Serializable {
    @EmbeddedId
    private UserPK userPK;
    private String password;
}

3、启动项目或者测试类,查看JPA自动建表语句,可以发现把 id 与 usrname 创建成联合主键了

create table sys_user (
    id bigint not null,
    username varchar(255) not null,
    password varchar(255),
    primary key (id, username)
)

4、普通Java项目测试

import entity.User;
import entity.UserPK;
import org.junit.Test;
import javax.persistence.*;

public class TestDemo {

    @Test
    public void testSave() {
        // 1.加载配置文件创建工厂(实体管理器工厂)对象
        EntityManagerFactory factory = Persistence.createEntityManagerFactory("myJpa");
        // 2.通过实体管理器工厂获取实体管理器
        EntityManager em = factory.createEntityManager();
        // 3.获取事务对象,开启事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        UserPK userPK = new UserPK();
        userPK.setId(1L);
        userPK.setUsername("admin");
        User user = new User();
        user.setUserPK(userPK);
        user.setPassword("password");
        em.persist(user);
        tx.commit();

        em.refresh(user);
        User user_selelct = em.find(User.class, userPK);
        System.out.println(user_selelct);

        // 6.释放资源
        em.close();
        factory.close();
    }
}

输出日志:

Hibernate: 
    insert 
    into
        sys_user
        (password, id, username) 
    values
        (?, ?, ?)
Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.username as username2_0_0_,
        user0_.password as password3_0_0_ 
    from
        sys_user user0_ 
    where
        user0_.id=? 
        and user0_.username=?
User(userPK=UserPK(id=1, username=admin), password=password)

5、SpringBoot项目测试,查看UserRepository接口定义查询方法

@Repository
public interface UserRepository extends JpaRepository<User, UserPK> {
    // 只查找一个条件(只查询复合主键中的一个字段)
    List<User> findByIdUsername(String username);

    // 查找两个条件(如果有三个复合主键可以这样使用,如果只有两个复合主键就直接findById就行了)
    List<User> findByIdIdAndUsername(String id,String username);
    
    // 查询所有复合主键就直接findById就行
    User findById(UserPK, userpk);
}

GeneratedValue

请参考本人的其他篇章,Hibernate JPA 主键策略的所有文章

@GeneratedValue主要与@Id一同使用,定义主键的生成策略,通过strategy属性指定。

元数据属性说明:

  • strategy(表示主键生成策略):
    1. GenerationType.AUTO:JPA自动选择合适的策略,是默认选项
    2. GenerationType.IDENTITY: 采用数据库ID自增长的方式生成主键值,一般用于MySQL数据库,Oracle不支持这种方式
    3. GenerationType.SEQUENCE:通过序列产生主键,通过@SequenceGenerator注解指定序列名,MySql不支持这种方式
    4. GenerationType.TABLE:通过表产生主键,会生成一张表模拟序列产生主键,该策略通用性强易于数据库的移植,效率低
  • generator(表示生成器的名字):这个属性通常和ORM框架相关。例如:Hibernate可以指定uuid等主键生成方式(要和@SequenceGenerator(name = “seq_tbl_user”, sequenceName = “seq_tbl_user”, allocationSize = 1)注解配合使用,其中name指定生成器的名字(与generator的值一样),sequenceName指定数据库中定义序列的名字,allocationSize指定序列每次增长1 )
@javax.persistence.Id
@javax.persistence.GeneratedValue(generator="xxx",strategy=GenerationType.AUTO)

strategy:表示主键生成策略,有如下四种方式:
方式一:@GeneratedValue(strategy=GenerationType.AUTO) 默认策略,生成方式取决于底层的数据库。
方式二:@GeneratedValue(strategy = GenerationType.IDENTITY)指定“自动增长”策略,适用于MySQL。 
方式三:@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_tbl_user")
       指定“序列”策略,适用于Oracle。其中generator表示生成器的名字,这个属性通常和ORM框架相关。
       例如,Hibernate可以指定uuid等主键生成方式(要和@SequenceGenerator(name = "seq_tbl_user", 
       sequenceName = "seq_tbl_user", allocationSize = 1)注解配合使用,其中name指定生成器的名 
       字(与generator的值一样),sequenceName指定数据库中定义序列的名字,allocationSize指定序
       列每次增长1 )
方式四:@GeneratedValue(strategy=GenerationType.TABLE)  使用一个特定的数据库表格来保存主键

SequenceGenerator

请参考本人的其他篇章,Hibernate JPA 主键策略:SEQUENCE 策略

GenericGenerator

请参考本人的其他篇章,Hibernate JPA 主键策略:自定义主键生成器

TableGenerator

请参考本人的其他篇章,Hibernate JPA 主键策略:TABLE 策略

3、嵌入注解

@Embedded 与 @Embeddable:当一个实体类要在多个不同的实体类中进行使用,而其不需要生成数据库表

  1. @Embeddable:注解在类上,表示此类是可以被其他类嵌套
  2. @Embedded:注解在属性上,表示嵌套被@Embeddable注解的同类型类

@AttributeOverride、@AttributeOverrides 注解重定义其父类或者嵌套类属性映射到数据库表中字段

Embeddable

默认情况下,JPA 持续性提供程序假设每个实体均持久保存到它自己的数据库表。

使用 @Embeddable 批注指定一个类,该类的实例存储为拥有实体的固有部分并共享该实体的身份。嵌入对象的每个持久属性或字段都将映射到实体的数据库表。此批注没有属性。

下面使用此注解批注 实体类UserCommon.java,然后可以使用 @Embedded 或 @EmbeddedId 把UserCommon 注入另一个实体类中当做其属性。(可以理解为单独抽离几个字段出来独立到另一个类中)

@Data
@Embeddable
public class UserCommon {
    @Column(name = "startDate")
    private java.util.Date startDate;
    private java.util.Date endDate;
}

PS:Embeddable一般与Embedded或EmbeddedId组合使用:@Embeddable + @Embedded、@Embeddable + @EmbeddedId

Embedded

使用 @Embedded 批注指定一个持久字段,该字段必须为引用类型,且还被 @Embeddable 批注。默认情况下 @Embeddable 类中指定的列定义适用于 @Embedded 类。如果要覆盖这些列定义可以使用 @AttributeOverride。

此批注没有属性。下面使用@Embeddable + @Embedded组合定义代码示例:

1、使用@Embeddable定义公共实体类

@Data
@Embeddable
public class UserCommon {
    @Column(name = "startDate")
    private java.util.Date startDate;
    private java.util.Date endDate;
}

2、表映射的持久类(持久类里要有一个注解@Embedded修饰主键的成员变量,其他的和普通的Entity类一样。它所对应复合主键类需要使用@Embeddable注解)

@Data
@Entity
@Table(name = "sys_user")
public class User implements Serializable {
    @EmbeddedId
    private UserPK userPK;
    @Embedded
    private UserCommon userCommon;
    private String password;
}

3、启动项目或者测试类,查看JPA自动建表语句,可以发现把 startDate 与 endDate 也创建到实体类 User 映射的数据表中了

create table sys_user (
    id bigint not null,
    username varchar(255) not null,
    password varchar(255),
    endDate timestamp,
    startDate timestamp,
    primary key (id, username)
)

备注:正常用法一般都是 @Embedded + @Embeddable 同时使用,实际上单独使用任意一个,效果也是一样(可读性差)

MappedSuperclass

@MappedSuperclass:(很重要)实现将实体类的多个属性分别封装到不同的非实体类中

  1. 注解的类将不是完整的实体类,不会映射到数据库表,但其属性将映射到子类的数据库字段
  2. 注解的类不能再标注@Entity或@Table注解,也无需实现序列化接口
  3. 注解的类继承另一个实体类或标注@MappedSuperclass类,他可使用@AttributeOverride或@AttributeOverrides注解重定义其父类属性映射到数据库表中字段

当我们在定义多个实体类时,可能会遇到这几个实体类都有几个共同的属性,这时就会出现很多重复代码。 这时我们可以选择用注解@MappedSuperclass。编写一个父类(基类),将这些共同属性放到这个父类中,并且在父类上加上@MappedSuperclass注解。

使用方法和注意事项:

  • @MappedSuperclass 注解使用在父类上面,是用来标识父类的
  • @MappedSuperclass 标识的类不是一个完整的实体类,它不会映射到数据库表,但是它的属性都将映射到其子类的数据库字段中
  • @MappedSuperclass 标识的类不能再有@Entity或@Table,也无需实现序列化接口,但其子类可以有@Entity或@Table

该批注没有属性。可以在子类中使用 @AttributeOverride 或 @AssociationOverride 批注来覆盖超类的映射配置。

@Data
@Entity
@Table(name = "sys_user")
public class User extends BaseEntity implements Serializable {
    private String admin;
    private String password;
}

@Data
@MappedSuperclass
abstract class BaseEntity {
    @Id
    private Long id; // 数据库主键
    @Column(name = "creation_time")
    private java.util.Date creationTime; //创建时间
    @Column(name = "update_time")
    private java.util.Date updateTime; //修改时间
}

启动项目或者测试类,查看JPA自动建表语句,可以发现 id、creation_time、update_time 字段也新增了

create table sys_user (
    id bigint not null,
    creation_time timestamp,
    update_time timestamp,
    admin varchar(255),
    password varchar(255),
    primary key (id)
)

AttributeOverride

@AttributeOverride 批注是用来覆盖 @Embeddable 类或者 @MappedSuperclas 类中字段的列属性 (包含要覆盖的@Embeddable类中字段名name和新增的@Column字段的属性;)

元数据属性说明:

  • name:如果使用了基于属性的访问,则映射的为嵌入对象中的属性名称,如果使用了基于字段的访问,则映射的为字段名称
  • column:映射到持久属性的 @Column。映射类型将与可嵌套类或映射超类中定义的类型相同

1、下面使用 @AttributeOverride 注解重写 @Embeddable 批注的类中的属性字段。为了方便查看使用了内部类。

@Data
@Entity
@Table(name = "sys_user")
public class User implements Serializable {
    @EmbeddedId
    private UserPK userPK;
    @Embedded
    @AttributeOverrides({ //把属性映射的列名称进行重写,startDate 和 endDate 是UserCommon类中的字段
            @AttributeOverride(name = "startDate", column = @Column(name = "start_date", nullable = false)),
            @AttributeOverride(name = "endDate", column = @Column(name = "end_date", nullable = false))
    })
    private UserCommon userCommon;
    private String password;

    @Data
    @Embeddable
    public static class UserCommon implements Serializable {
        @Column(name = "startDate")
        private java.util.Date startDate;
        private java.util.Date endDate;
    }

    @Data
    @Embeddable
    public static class UserPK implements Serializable {
        @Column(name = "id")
        private Long id;
        @Column(name = "username")
        private String username;
    }
}

2、启动项目或者测试类,查看JPA自动建表语句,可以发现startDate与endDate字段都被重写了,并且加了不能为空的约束。

create table sys_user (
    id bigint not null,
    username varchar(255) not null,
    password varchar(255),
    end_date timestamp not null,
    start_date timestamp not null,
    primary key (id, username)
)

3、下面使用 @AttributeOverride 注解重写 @MappedSuperclass 批注的类中的属性字段。

@Data
@Entity
@Table(name = "sys_user")
@AttributeOverrides({ // 把属性映射的列名称进行重写,creationTime 和 updateTime 是BaseEntity类中的字段
        @AttributeOverride(name = "creationTime", column = @Column(name = "create_date", nullable = false)),
        @AttributeOverride(name = "updateTime", column = @Column(name = "update_date", nullable = false))
})
public class User extends BaseEntity implements Serializable {
    private String admin;
    private String password;
}

@Data
@MappedSuperclass
abstract class BaseEntity {
    @Id
    private Long id; // 数据库主键
    @Column(name = "creation_time")
    private java.util.Date creationTime; //创建时间
    @Column(name = "update_time")
    private java.util.Date updateTime; //修改时间
}

4、启动项目或者测试类,查看JPA自动建表语句,可以发现creationTime与updateTime字段都被重写了,并且加了不能为空的约束。

create table sys_user (
    id bigint not null,
    create_date timestamp not null,
    update_date timestamp not null,
    admin varchar(255),
    password varchar(255),
    primary key (id)
)

AttributeOverrides

@AttributeOverrides 里面只包含了@AttributeOverride 类型数组,用法直接参考@AttributeOverride的即可。

AssociationOverride

默认情况下,JPA 持续性提供程序自动假设子类继承超类中定义的持久属性及其关联映射。

如果继承的列定义对实体不正确(例如:继承的列名与已经存在的数据模型不兼容或作为数据库中的列名无效),请使用 @AssociationOverride 批注自定义从 @MappedSuperclass 或 @Embeddable 继承的 @OneToOne 或 @ManyToOne 映射,以更改与字段或属性关联的 @JoinColumn。如果有多个要进行的 @AssociationOverride 更改,则必须使用 @AssociationOverrides。

要自定义基本映射以更改它的 @Column,请使用 @AttributeOverride。

元数据属性说明:

  • name:如果使用了基于属性的访问,则映射的为嵌入对象中的属性名称,如果使用了基于字段的访问,则映射的为字段名称
  • joinColumns:要指定映射到持久属性的连接列,请将 joinColums 设置为 JoinColumn 实例的数组,映射类型将与可嵌套类或映射的超类中定义的类型相同。

1、这里使用@MappedSuperclass示例,先测试没有使用@AssociationOverride注解的效果。

import lombok.Data;
import javax.persistence.*;

@Data
@Entity
@Table(name = "sys_user")
public class User extends BaseEntity implements java.io.Serializable {
    @Id
    private Long id;
    private String admin;
    private String password;
}

@Data
@MappedSuperclass
abstract class BaseEntity {
    @ManyToOne
    @JoinColumn(name="address_id")
    private Address address;
}

@Data
@Entity
@Table(name = "sys_address")
class Address implements java.io.Serializable {
    @Id
    @Column(name = "address_id")
    private Long addressId;
    private String country;
}

查看输出日志:

create table sys_address (
    address_id bigint not null,
    country varchar(255),
    primary key (address_id)
);
create table sys_user (
    id bigint not null,
    admin varchar(255),
    password varchar(255),
    address_id bigint,
    primary key (id)
);
alter table sys_user 
       add constraint FK9abddsodne05nqpe2if8gjekl 
       foreign key (address_id) 
       references sys_address;

2、再使用@MappedSuperclass + AssociationOverride示例查看效果。

import lombok.Data;
import javax.persistence.*;

@Data
@Entity
@Table(name = "sys_user")
@AssociationOverride(name="address", joinColumns=@JoinColumn(name="fk_address_id"))
public class User extends BaseEntity implements java.io.Serializable {
    @Id
    private Long id;
    private String admin;
    private String password;
}

@Data
@MappedSuperclass
abstract class BaseEntity {
    @ManyToOne
    @JoinColumn(name="address_id")
    private Address address;
}

@Data
@Entity
@Table(name = "sys_address")
class Address implements java.io.Serializable {
    @Id
    private Long id;
    private String country;
}

查看输出日志:

create table sys_address (
    id bigint not null,
    country varchar(255),
    primary key (id)
);
create table sys_user (
    id bigint not null,
    admin varchar(255),
    password varchar(255),
    fk_address_id bigint,
    primary key (id)
);
alter table sys_user 
       add constraint FKmas1faywy7oyb4vr78hhm6kp0 
       foreign key (fk_address_id) 
       references sys_address;

从日志中可以看出@AssociationOverride的作用主要是重写了@JoinColumn注解的属性。

AssociationOverrides

@AssociationOverrides 里面只包含了@AssociationOverride 类型数组,用法直接参考@AssociationOverride的即可。

简单对比@AssociationOverride与AttributeOverride的作用与区别:

  1. @AssociationOverride主要重写@MappedSuperclass 或 @Embeddable 继承的 @JoinColumn 注解字段
  2. @AttributeOverride主要重写@MappedSuperclass 或 @Embeddable 继承的 @Column 注解字段

4、次用注解

SecondaryTable

@SecondaryTable将一个实体映射到多个数据库表中。

元数据属性说明:

  • name:表名
  • catalog:对应关系数据库中的catalog
  • schema:对应关系数据库中的schema
  • pkJoinColumns:定义一个PrimaryKeyJoinColumn数组,指定从表的主键列
  • UniqueConstraints:定义一个UniqueConstraint数组,指定需要建唯一约束的列
// 如果只多映射一个数据表的话可以只用@SecondaryTable
@Entity
@Table(name = "tb_customer")
@SecondaryTables({
    @SecondaryTable(name = "tb_address", pkJoinColumns = {@PrimaryKeyJoinColumn(name="address_id",referencedColumnName="id")}),
    @SecondaryTable(name = "tb_comments", pkJoinColumns = {@PrimaryKeyJoinColumn(name="comments_id",referencedColumnName="id")})
})
public class Customer implements Serializable {
    @Id
    private Long id;
    private String username;
    private String password;
    @Column(table = "tb_address")
    private String street; 
    @Column(table = "tb_address")
    private String city; 
    @Column(table = "tb_address")
    private String conutry; 
    @Column(table = "tb_comments")
    private String title; 
    @Column(table = "tb_comments")
    private String comments;
}

@Column中的table属性的值指定属性存储的哪张数据库表。没有用@Column中table值注解改变的属性,将会存在于默认tb_forum主表中。

mysql> desc tb_customer;
+----------+--------------+------+-----+---------+-------+
| Field    | Type         | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| id       | bigint(20)   | NO   | PRI | NULL    |       |
| password | varchar(255) | YES  |     | NULL    |       |
| username | varchar(255) | YES  |     | NULL    |       |
+----------+--------------+------+-----+---------+-------+
3 rows in set (0.02 sec)

mysql> desc tb_address;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| city       | varchar(255) | YES  |     | NULL    |       |
| conutry    | varchar(255) | YES  |     | NULL    |       |
| street     | varchar(255) | YES  |     | NULL    |       |
| address_id | bigint(20)   | NO   | PRI | NULL    |       |
+------------+--------------+------+-----+---------+-------+
4 rows in set (0.02 sec)

mysql> desc tb_comments;
+-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| comments    | varchar(255) | YES  |     | NULL    |       |
| title       | varchar(255) | YES  |     | NULL    |       |
| comments_id | bigint(20)   | NO   | PRI | NULL    |       |
+-------------+--------------+------+-----+---------+-------+
3 rows in set (0.03 sec)

SecondaryTables

当一个实体类映射到一个主表和多个从表时,用@SecondaryTables来定义各个从表的属性。

元数据属性说明:

  • columnNames:定义一个字符串数组,指定要建唯一约束的列名

案例可以参考上面@SecondaryTable的示例代码。

PrimaryKeyJoinColumn

@PrimaryKeyJoinColumn一般在三种情况下会用到(@PrimaryKeyJoinColumns是用来装@PrimaryKeyJoinColumn数组的)

  1. 继承
  2. 实体类映射到一个或多个从表。从表根据主表的主键列(列名为referencedColumnName值的列),建立一个类型一样的主键列,列名由name属性定义
  3. one-to-one关系,关系维护端的主键作为外键指向关系被维护端的主键,不再新建一个外键列

元数据属性说明:

  • name:列名
  • referencedColumnName:该列引用列的列名
  • columnDefinition:定义建表时创建此列的DDL

下面的代码说明Customer映射到两个表,主表tb_customer,从表tb_address,从表需要建立主键列address_id,该列和主表的主键列id除了列名不同,其他定义一样

@Entity
@Table(name = "tb_customer")
@SecondaryTable(name = "tb_address", pkJoinColumns = {@PrimaryKeyJoinColumn(name="address_id",referencedColumnName="id")})
public class Customer implements Serializable {
    @Id
    private Long id;
    private String username;
    private String password;
    @Column(table = "tb_address")
    private String street; 
    @Column(table = "tb_address")
    private String city; 
    @Column(table = "tb_address")
    private String conutry; 
}

下面的代码说明Customer和Address是一对一关系,Customer的主键列id作为外键指向Address的主键列address_id(这个本人暂时还没研究清楚)

@Entity
@Table(name = "tb_customer")
public class Customer {
    private Long customer_id;
    @OneToOne
    @PrimaryKeyJoinColumn(name = "customer_id", referencedColumnName="address_id")
    private Address address;
}

PrimaryKeyJoinColumns

如果实体类使用了复合主键,指定单个PrimaryKeyJoinColumn不能满足要求时,可以用PrimaryKeyJoinColumns来定义多个PrimaryKeyJoinColumn

元数据属性说明:

  • value: 一个PrimaryKeyJoinColumn数组,包含所有PrimaryKeyJoinColumn。

下面的代码说明了Employee和EmployeeInfo是一对一关系。他们都使用复合主键,建表时需要在Employee表建立一个外键,从Employee的主键列id,name指向EmployeeInfo的主键列INFO_ID和INFO_NAME

@Data
@IdClass(EmpPK.class)
@Entity
@Table(name = "EMPLOYEE")
public class Employee {
    private int id;
    private String name;
    private String address;

    @OneToOne(cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumns({
        @PrimaryKeyJoinColumn(name="id", referencedColumnName="INFO_ID"),
        @PrimaryKeyJoinColumn(name="name" , referencedColumnName="INFO_NAME")})
    EmployeeInfo info;
}
@Data
@IdClass(EmpPK.class)
@Entity
@Table(name = "EMPLOYEE_INFO")
public class EmployeeInfo {
    @Id
    @Column(name = "INFO_ID")
    private int id;
    @Id
    @Column(name = "INFO_NAME")
    private String name;
}

UniqueConstraint

@UniqueConstraint定义在@Table或@SecondaryTable元数据里,用来指定建表时需要建唯一约束的列。

元数据属性说明:

  1. name:定义的名称
  2. columnNames:定义一个字符串数组,指定要建唯一约束的列名
@Entity
@Table(name="tb_person",uniqueConstraints={@UniqueConstraint(columnNames={"person_id", "person_name"})})
public class Person {
    @Id
    private Long person_id;
    private String person_name;
}

OrderBy

@OrderBy 在加载数据的时候可以为其指定顺序。有时我们希望从数据库加载出来的集合对象是按一定方式排序的,这可以通过OrderBy来实现,默认是按对象的主键升序排列。

元数据属性说明:

  • value:字符串类型,指定排序方式
  • 格式为:fieldName1 [ASC|DESC],fieldName2 [ASC|DESC],...,(排序类型可以不指定,默认是ASC)

下面的代码说明Person和Book之间是一对多关系。集合books按照Book的age升序,name降序排列。

@Table(name="tb_person")
public class Person {
    @OneToMany(targetEntity = Book.class, cascade = CascadeType.ALL, mappedBy = "person")
    @OrderBy(name = "age asc, name desc")
    private List<Book> books = new ArrayList();
    
    @OneToMany(targetEntity = Detail.class, cascade = CascadeType.ALL, mappedBy = "person")
    @OrderBy // 默认使用主键排序
    private List<Detail> details = new ArrayList();
}

Version

@Version 指定实体类在乐观事务中的 version 属性。在实体类重新由EntityManager管理并且加入到乐观事务中时,保证完整性。每一个类只能有一个属性被指定为 version,version 属性应该映射到实体类的主表上。version支持(int,Integer,short,Short, long,Long,java.sql.Timestamp)类型的属性或字段。

使用起来非常方便,我们只需要在实体中添加一个字段,并添加@Version注解就可以了。加了@Version后,insert和update的SQL语句都会带上version的操作。当乐观锁更新失败的时候,会抛出异常org.springframework.orm.ObjectOptimisticLockingFailureException。我们自己进行业务处理。

@Version
@Column(name = "customer_version", columnDefinition="bigint default 0")
private Long version;

Lob

@Lob指定一个属性作为数据库支持的大对象类型在数据库中存储。使用LobType这个枚举来定义Lob是二进制类型还是字符类型。

元数据属性说明(较老的版本):

  • fetch: 定义这个字段是 lazy loaded 还是 eagerly fetched。数据类型是FetchType枚举,默认:LAZY(lazy loaded)
  • type: 定义这个字段在数据库中的JDBC数据类型。数据类型是LobType枚举,默认:BLOB
    • LobType.BLOB:二进制大对象,byte[] 或者 Serializable 类型可以指定为 BLOB
    • LobType.CLOB:字符型大对象,char[]、Character[]或String类型可以指定为 CLOB

新版本中该注解中没有任何参数了。下面的代码测试:

@Lob
private byte[] picture; // 默认是立即加载

@Lob
@Basic(fetch = FetchType.LAZY) // 修改成懒加载
private String description;

PS:Clob、Blob占用内存空间较大,一般配合@Basic(fetch=FetchType.LAZY)将其设置为延迟加载

Enumerated

@Enumerated虽然不常用却很重要,使用此注解映射枚举字段,以String类型或者Interger类型存入数据库

注入数据库的类型有两种:

  1. EnumType.ORDINAL:如果当前实体类字段为枚举类会转换成Interger类型存入数据库(默认)
  2. EnumType.STRING:如果当前实体类字段为枚举类会转换成String类型存入数据库
// Sex为枚举类
@Column
@Enumerated(EnumType.STRING)
private Sex sex;

ElementCollection

@ElementCollection集合映射,请参考:https://mp.weixin.qq.com/s/K8ori0frTguph18nrcVPAQ

NoRepositoryBean

@NoRepositoryBean:一般用作父类的repository,有这个注解,Spring不会去实例化该repository。

5、审计功能(Spring Data)

参考文献 & 鸣谢:https://blog.csdn.net/qq_28804275/article/details/84801457

@CreatedDate、@CreatedBy、@LastModifiedDate、@LastModifiedBy:表示字段为:创建时间字段(insert自动设置)、创建用户字段(insert自动设置)、最后修改时间字段(update自定设置)、最后修改用户字段(update自定设置)

@CreatedDate、@CreatedBy、@LastModifiedDate、@LastModifiedBy 注解的用法:

  1. 申明实体类并在头部加注解:@EntityListeners(AuditingEntityListener.class)
  2. 在实体类中属性中加上面四种注解:@CreatedDate、@LastModifiedDate、@CreatedBy、@LastModifiedBy
  3. 在SpringBoot启动类中加此注解:@EnableJpaAuditing
  4. 如上操作默认只能实现@CreatedDate、@LastModifiedDate的自动赋值,@CreatedBy、@LastModifiedBy自定义添加修改用户字段,需要配置转换类实现AuditorAware接口才能使它们生效

如下是操作示例:

1、在实体类上加上注解 @EntityListeners(AuditingEntityListener.class),在相应的字段上添加对应的时间和用户注解 @CreatedDate、@LastModifiedDate、@CreatedBy、@LastModifiedBy(PS:注意:日期类型可以用 Date 也可以是 Long

package com.example.jpa.entity;
import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.io.Serializable;

@Data
@Entity
@Table(name = "sys_user")
@EntityListeners(AuditingEntityListener.class)
public class User implements Serializable {
    @Id
    private Long id;

    @CreatedDate
    @Column(updatable = false, nullable = false)
    private java.util.Date createTime; // 创建时间

    @LastModifiedDate
    @Column(nullable = false)
    private Long updateTime; // 更新时间

    @CreatedBy
    private String createBy; // 创建人

    @LastModifiedBy
    private String lastModifiedBy; // 最后修改人
}

2、配置实现AuditorAware接口,以获取用户字段需要插入的信息:

package com.example.jpa.config;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
import java.util.Optional;

/**
 * @ClassName: JpaAuditorAware
 * @Description:根据你需要返回的类型修改这个T,比如我需要返回的是字符串,就是String。需要注意的是,类需要加上@Component以便Spring扫描到,否则不起作用。
 */
@Component
public class JpaAuditorAware implements AuditorAware<String> {
    @Override
    public Optional<String> getCurrentAuditor() {
        String userId = null; // SecurityUtils.getCurrentUserId(); 正常项目安装项目逻辑获取用户ID
        if (userId != null) {
            return Optional.of(userId);
        } else {
            return Optional.of("admin_user");
        }
    }
}

3、在启动类中添加注解@EnableJpaAuditing,注意如果有多个AuditorAware接口实现类,需要在@EnableJpaAuditing内指定实现类

@SpringBootApplication
@EnableJpaAuditing
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

4、启动项目和单元测试查看效果

@Test
@Transactional // 在测试类对于事务提交方式默认的是回滚
@Rollback(false) // 取消自动回滚
public void save() {
    User user = new User();
    user.setId(1L);
    User save = userRepository.save(user);
    System.out.println(save);
}

5、查看输出日志

Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.create_by as create_b2_0_0_,
        user0_.create_time as create_t3_0_0_,
        user0_.last_modified_by as last_mod4_0_0_,
        user0_.update_time as update_t5_0_0_ 
    from
        sys_user user0_ 
    where
        user0_.id=?
User(id=1, createTime=Sat Feb 05 17:07:15 CST 2022, updateTime=1644052035337, createBy=admin_user, lastModifiedBy=admin_user)
Hibernate: 
    insert 
    into
        sys_user
        (create_by, create_time, last_modified_by, update_time, id) 
    values
        (?, ?, ?, ?, ?)

6、关联关系注解

1.配置多表联系注解介绍
@OneToOne(一对一)                  一对一映射
    targetEntityClass:             指定另一方类的字节码
    cascade:                       级联操作
        CascadeType.MERGE           级联更新
        CascadeType.PERSIST         级联保存
        CascadeType.REFRESH         级联刷新
        CascadeType.REMOVE          级联删除
        CascadeType.ALL             级联上述4种操作
    fetch:                         抓取策略
      FetchType.LAZY:              延迟加载(默认)
      FetchType.EAGER:             迫切查询(多表连接查询)
    mappedBy:                      放弃外键维护
    orphanRemoval:                 是否使用孤儿删除

@OneToMany(一对多)
    targetEntityClass:             指定多方类的字节码
    cascade:                       指定要使用的级联操作
    fetch:                         指定是否采用延迟加载
    mappedBy:                      指定从表实体类中引用主表对象的名称
    orphanRemoval:                 是否使用孤儿删除

@ManyToOne(多对一)
    targetEntityClass:             指定一的一方实体类字节码
    cascade:                       指定要使用的级联操作
    fetch:                         指定是否采用延迟加载
    optional:                      关联是否可选。如果设置为false,则必须始终存在非空关系

@ManyToMany(多对多)
    targetEntityClass:             指定另一方类的字节码
    cascade:                       配置级联操作
    fetch:                         配置延迟加载和立即加载
    mappedBy:                      放弃外键维护
    
    
2:配置外键关系的注解
@JoinColumn:用于定义主键字段和外键字段的对应关系。
    name:                          指定外键字段的名称
    referencedColumnName:          指定引用主表的主键字段名称(注意是数据库字段不是实体类属性名)
    unique:                        是否唯一。默认值不唯一
    nullable:                      是否允许为空。默认值允许
    insertable:                    是否允许插入。默认值允许
    updatable:                     是否允许更新。默认值允许
    columnDefinition:              列的定义信息
    table
    foreignKey

@JoinTable(针对中间表的设置)
    name:                          配置中间表的名称
    joinColumns:                   中间表的外键字段关联当前实体类所对应表的主键字段
        @JoinColumn:
            name:                  本类的外键
            referencedColumnName:  本类与外键(表)对应的主键
    inverseJoinColumn:             中间表的外键字段关联对方表的主键字段
        @JoinColumn:               
            name:                  对方类的外键
            referencedColumnName:  对方类与外键(表)对应的主键
            
@OrderBy(也属于关联关系注解)主要用来排序外键列集合对象

3:首先确定多表之间的关系:
    一对一和一对多:
        一的一方称为主表,而多的一方称为从表,外键就要建立在从表上,它们的取值的来源主要来自主键
    多对多:
        这个时候需要建立一个中间表,中间表中最少由2个字段组成,这2个字段作为外键指向2张表的主键又组成了联合主键

7、生命周期回调方法

  1. Java持久性API(JPA)第7讲——实体生命周期及生命周期回调方法_seloba-CSDN博客
  2. 【原创】JPA中@PrePersist和@PreUpdate的用法_DCTANT的博客-CSDN博客_jpa prepersist
  3. @PreUpdate 和@PrePersist_sunrainamazing的博客-CSDN博客_prepersist
  4. JPA @PostPersist 等注解的使用场景和使用方法 - 码农教程 (manongjc.com)

我们在使用JPA对数据库进行操作的时候,我们时常会出现数据库字段设置未不能为空,而我们保存的字段为null导致程序报错。这个时候我们就可以使用:@PrePersist、@PostPersist 等注解回调方法来解决问题。该注释可以应用于实体类,映射超类或回调监听器类的方法。

回调方法是附加到实体生命周期事件的用户定义方法,并且在发生这些事件时由JPA自动调用。可以发现有很多类似的注解可以使用:

  1. @PrePersist:在新实体持久化之前(添加到EntityManager之前,即:保存操作之前)
  2. @PostPersist:在数据库中存储新实体(在commit或flush,即:保存操作之后)
  3. @PreUpdate:当一个实体被识别为被修改时EntityManager(即:更新数据之前)
  4. @PostUpdate:更新数据库中的实体(在commit或flush,即:更新数据之后)
  5. @PreRemove:在EntityManager中标记要删除的实体时(即:删除数据之前)
  6. @PostRemove:从数据库中删除实体(在commit或flush,即:删除数据之后)
  7. @PostLoad:从数据库中检索实体后(即:查询完数据后)
  8. @EntityListeners:指定外部生命周期事件实现类

在Entity实体类中进行代码测试

1、创建User实体类Entity,在Entity实体内加回调方法时,回调方法形参可有可无(外部类定义时必须要有)

package entity;
import lombok.Data;
import javax.persistence.*;

@Data
@Entity
@Table(name = "sys_user")
public class User {
    @Id
    private Long id;
    private String username;
    private String password;

    @PostLoad
    public void postLoad(){
        System.out.println("PreLoad 生命周期方法被调用! ");
    }
    @PrePersist
    public void prePersist(){
        System.out.println("PrePersist 生命周期方法被调用! ");
    }
    @PreUpdate
    public void preUpdate(){
        System.out.println("PreUpdate 生命周期方法被调用! ");
    }
    @PreRemove
    public void preRemove(){
        System.out.println("PreRemove 生命周期方法被调用! ");
    }
    @PostPersist
    public void postPersist(){
        System.out.println("PostPersist 生命周期方法被调用! ");
    }
    @PostUpdate
    public void postUpdate(){
        System.out.println("PostUpdate 生命周期方法被调用! ");
    }
    @PostRemove
    public void postRemove(){
        System.out.println("PostRemove 生命周期方法被调用! ");
    }
}

2、进行CRUD单元测试

import entity.User;
import org.junit.Test;
import javax.persistence.*;

public class TestDemo {

    @Test
    public void testSave() {
        // 1.加载配置文件创建工厂(实体管理器工厂)对象
        EntityManagerFactory factory = Persistence.createEntityManagerFactory("myJpa");
        // 2.通过实体管理器工厂获取实体管理器
        EntityManager em = factory.createEntityManager();
        // 3.获取事务对象,开启事务
        EntityTransaction tx = em.getTransaction();

        // 开启事物,保存操作
        tx.begin();
        User user = new User();
        user.setId(1L);
        user.setUsername("admin");
        user.setPassword("password");
        em.persist(user);
        tx.commit();

        // 查询操作,查询同一个数据需要先清理一级缓存,不然不会发送查询SQL
        em.refresh(user);
        User user_select = em.find(User.class, 1L);

        // 开启事物,修改操作
        tx.begin();
        user_select.setUsername("root");
        em.merge(user_select);
        tx.commit();

        // 开启事物,删除操作
        tx.begin();
        em.remove(user_select);
        tx.commit();
        // 6.释放资源
        em.close();
        factory.close();
    }
}

3、输出日志

PrePersist 生命周期方法被调用! 
Hibernate: 
    insert 
    into
        sys_user
        (password, username, id) 
    values
        (?, ?, ?)
PostPersist 生命周期方法被调用! 
Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.password as password2_0_0_,
        user0_.username as username3_0_0_ 
    from
        sys_user user0_ 
    where
        user0_.id=?
PreLoad 生命周期方法被调用! 
PreUpdate 生命周期方法被调用! 
Hibernate: 
    update
        sys_user 
    set
        password=?,
        username=? 
    where
        id=?
PostUpdate 生命周期方法被调用! 
PreRemove 生命周期方法被调用! 
Hibernate: 
    delete 
    from
        sys_user 
    where
        id=?
PostRemove 生命周期方法被调用! 

@EntityListeners:指定外部生命周期事件实现类,在Entity实体类中进行代码测试

1、创建User实体类(这里为了方便查看,使用static建了一个内部类,实际上登录外部新建一个类)
PS:在外部类中使用@PrePersist等注解时,必须要注意:回调方法中要有Object entity参数,不然会报错。

package entity;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;

@Data
@Entity
@Table(name = "sys_user")
@EntityListeners(User.UserListener.class)
public class User implements Serializable {
    @Id
    private Long id;
    private String username;
    private String password;

    public static class UserListener{
        @PostLoad
        public void postLoad(Object user){
            System.out.println("PreLoad 生命周期方法被调用! ");
        }
        @PrePersist
        public void prePersist(Object user){
            System.out.println("PrePersist 生命周期方法被调用! ");
        }
        @PreUpdate
        public void preUpdate(Object user){
            System.out.println("PreUpdate 生命周期方法被调用! ");
        }
        @PreRemove
        public void preRemove(Object user){
            System.out.println("PreRemove 生命周期方法被调用! ");
        }
        @PostPersist
        public void postPersist(Object user){
            System.out.println("PostPersist 生命周期方法被调用! ");
        }
        @PostUpdate
        public void postUpdate(Object user){
            System.out.println("PostUpdate 生命周期方法被调用! ");
        }
        @PostRemove
        public void postRemove(Object user){
            System.out.println("PostRemove 生命周期方法被调用! ");
        }
    }
}

2、进行CRUD单元测试与输出日志(单元测试可以复用上面的,日志输出与上面也是一摸一样)

两种方式的区别:内部Entity时回调方法形参可有可无,但是外部实现时回调函数形参必须要有

8、Convert 转换器(重点)

JPA实体属性类型转换器: @Convert + AttributeConverter,通过 @Convert 注解指定自定义转换器,可用于实体属性类型与数据库字段类型之间的相互转换,便于将数据存储至数据库或从数据库读取数据。

@Repeatable(Converts.class)
@Target({METHOD, FIELD, TYPE}) @Retention(RUNTIME)
public @interface Convert {
    Class converter() default void.class;
    String attributeName() default "";
    boolean disableConversion() default false; // 用于禁用自动应用或继承的转换器。如果为true,则不应指定converter
}

自定义转换器详解:

  1. 实体类的字段添加@Converter注解,标识为转换器
  2. 编写一个转换类,实现 AttributeConverter 接口,X 为实体属性类型,Y 为数据库字段类型
  3. 转换类实现接口后需要重写 convertToDatabaseColumn()和convertToEntityAttribute()方法
  4. Y convertToDatabaseColumn(X) 的作用:将实体属性X转化为Y存储到数据库中,即插入和更新操作时执行
  5. X convertToEntityAttribute(Y) 的作用:将数据库中的字段Y转化为实体属性X,即查询操作时执行

操作示例:

1、创建Convert,需要实现AttributeConverter接口

/**
 * 该转换器主要处理遇到null转为空字符串
 */
@Converter
public class CustConvert implements AttributeConverter<String, String> {
    /**
     * 将实体属性x转化为y存储到数据库中,即插入和更新操作时执行
     */
    @Override
    public String convertToDatabaseColumn(String attribute) {
        return Objects.isNull(attribute) ? "" : attribute;
    }

    /**
     * 将数据库中的字段y转化为实体属性x,即查询操作时执行
     */
    @Override
    public String convertToEntityAttribute(String dbData) {
        return Objects.isNull(dbData) ? "" : dbData;
    }
}

2、实体类对象使用@Convert指定转换器

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "tb_convert")
public class CustConvertVO {
    @Id
    private Long id;
    @Convert(converter = CustConvert.class)
    private String username;
}

3、测试操作

public class JpaConvertTest {
    /**
     * 运行之前,修改hibernate.hbm2ddl.auto=create
     */
    @Test
    public void testSave() {
        // 获取实体管理器工厂
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("myJpa");
        // 获取实体管理器
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        // 获取事务
        EntityTransaction transaction = entityManager.getTransaction();
        // 开启事务
        transaction.begin();
        // 创建实体对象并保存
        entityManager.persist(new CustConvertVO(1L,null));
        entityManager.persist(new CustConvertVO(2L, "sam"));
        // 提交事务
        transaction.commit();
        entityManager.clear();
        System.out.println(entityManager.find(CustConvertVO.class, 1L));
        System.out.println(entityManager.find(CustConvertVO.class, 2L));
        // 释放资源
        entityManager.close();
        entityManagerFactory.close();
    }
}

4、查看日志:

Hibernate: 
    insert 
    into
        tb_convert
        (username, id) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        tb_convert
        (username, id) 
    values
        (?, ?)
Hibernate: 
    select
        custconver0_.id as id1_3_0_,
        custconver0_.username as username2_3_0_ 
    from
        tb_convert custconver0_ 
    where
        custconver0_.id=?
CustConvertVO(id=1, username=)
Hibernate: 
    select
        custconver0_.id as id1_3_0_,
        custconver0_.username as username2_3_0_ 
    from
        tb_convert custconver0_ 
    where
        custconver0_.id=?
CustConvertVO(id=2, username=sam)

9、Hibernate-Validation 校验注解

由于Hibernate-Validation篇幅教程过于多,后面可能会单独教程笔记,暂时请参考如下教程:

  1. SpringBoot使用Validation校验参数(CSDN):https://blog.csdn.net/justry_deng/article/details/86571671
  2. SpringBoot使用Hibernate-Validator校验(博客园):https://www.cnblogs.com/mr-yang-localhost/p/7812038.html
  3. SpringBoot中在除Controller层 使用Validation的方式(博客园):https://www.cnblogs.com/gxc6/p/11407599.html
  4. Spring Boot 参数校验(CSDN-废物大师兄):https://www.cnblogs.com/cjsblog/p/8946768.html
  5. 使用Spring Validation优雅地校验参数(博客园):https://www.cnblogs.com/zhengxl5566/p/13398546.html

你可能感兴趣的:(从零开始学,Spring,Data,JPA,hibernate,java,数据库)