JPA随记1

1. EntityManager

EntityManager不是线程安全的,而EntityManagerFactory是线程安全的,如果需要向Spring容器注入EntityManager对象,可使用@PersistenceContext注解,它保证了每个线程使用的EntityManager是独立的。

@PersistenceContext
private EntityManager em;

2. 映射关系注解

2.1 一对一关系

/**
 * 客户表,每个客户拥有一个账户,为一对一关系
 */
@Entity                            // 作为hibernate 实体类
@Table(name = "tb_customer")       // 映射的表明
@Data
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long custId;    //客户的主键
    @Column(name = "cust_name")
    private String custName;  //客户名称
    @Column(name = "cust_address")
    private String custAddress;//客户地址
    @OneToOne(mappedBy = "customer", cascade = CascadeType.ALL, 
              fetch = FetchType.LAZY, orphanRemoval = true, optional=false)
    @JoinColumn(name = "account_id")   // 设置外键的字段名
    private Account account;
}
/**
 * 账户表,每个账户对应一个客户,一对一关系
 */
@Entity
@Table(name = "tb_account")
@Data
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;  // 未采用@Column注解指定字段名
    private String password;
    @OneToOne                       // 一对一映射
    @JoinColumn(name = "customer_id") // 外键ID
    private Customer customer;
}
  1. @Column注解不是必须的,若未指定该注解的name值,则采用属性名作为数据库字段名;
  2. @JointColumn用于指定表中外键的名称,此时功能类似@Column,但注意它修饰属性类型是另一个Entity类;
  3. @OneToOne注解可以仅用于其中一个实体类,也可以同时应用于两个实体类(比如希望在查询一对一的两个实体之一时,可以加载另一个实体,两个方向都希望可以如此操作的时候),在没有应用该注解的mappedBy属性时,Hibernate会在标注了@OneToOne的实体类对应的数据库表创建外键约束,即若两个实体类都应用了@OneToOne注解的情况下,则两张表都会创建另一张表的外键约束,此时两张互相存在外键约束的表,在删除数据和删除表的时候,会相对麻烦,可以通过该注解的mappedBy属性指定另一个实体的外键属性名称,表示当前实体对应的表不维护外检约束,由另一个实体对应的表维护外键约束;
    外键约束

3.1 @OneToOne注解的cascade属性表示在操作当前实体的时候,是否需要级联操作对应的外键记录,相关的操作属性值有:ALLPERSISTMERGEREMOVEREFRESHDETACH;(查询默认就支持级联操作,没有相关属性值)
3.2 @OneToOne注解的fetch属性表示是否启用懒加载,相关的属性值有:LAZYEAGER;
3.3 @OneToOne注解的orphanRemoval属性值为true的时候,表示当被@OneToOne注解修饰的外键实体被当前实体设置为null时,进行数据库操作时,JPA会级联删除对应的外键记录;
3.4 @OneToOne注解的optional属性值表示当前实体是否能够将对应的外键实体设置为null与否。

2.2 一对多关系

场景:一个客户可以有多条消息

@Entity     // 作为hibernate 实体类
@Table(name = "tb_customer")       // 映射的表明
@Data
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long custId; //客户的主键
    @Column(name = "cust_name")
    private String custName;//客户名称
    @Column(name = "cust_address")
    private String custAddress;//客户地址
    // 一对多
    // fetch 默认是懒加载   懒加载的优点( 提高查询性能)
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private List<Message> messages;
}
@Entity
@Table(name = "tb_message")
@Data
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String info;

    public Message(String info) {
        this.info = info;
    }

    // 一定要有、否则查询就会有问题
    public Message() {
    }
}
  1. @OneToMany注解表示一对多的外键关系,由于是一对多,它应该放置在表示一端的实体类中,修饰的属性应该是集合类型,比如本例的List<Message>,而Hibernate会将对应的外键字段创建到多端的实体类对应的数据库表上,同时也会这张表创建对应的外键约束;
  2. @OneToMany注解的fetch属性默认是LAZY,即默认懒加载,这是因为一对多如果采用即时加载,在需要关联很多外键表但这些数据在业务上应用不到的情况下将会浪费很多性能在做外连接操作上。

2.3 多对一关系

场景:一个客户可以有多条消息

@Entity     // 作为hibernate 实体类
@Table(name = "tb_customer")       // 映射的表明
@Data
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long custId; //客户的主键
    @Column(name = "cust_name")
    private String custName;//客户名称
    @Column(name = "cust_address")
    private String custAddress;//客户地址
    // 一对多
    // fetch 默认是懒加载   懒加载的优点( 提高查询性能)
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private List<Message> messages;
}
@Entity
@Table(name = "tb_message")
@Data
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String info;

    public Message(String info) {
        this.info = info;
    }

    public Message(String info, Customer customer) {
        this.info = info;
        this.customer = customer;
    }

    // 一定要有、否则查询就会有问题
    public Message() {
    }

    // 多对一
    @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    @JoinColumn(name = "customer_id")
    private Customer customer;
}
  1. 如果需要持久化多端的实体集合,建议应用多端实体的Repository进行持久化操作,如果使用一端的实体类进行持久化,Hibernate会先插入一端的实体,再插入多端的实体集合,最后再对每一条多端的记录进行update操作,维护多端记录的外键;


    一端插入

    多端插入
  2. 根据查询条件场景的不同,可以通过多端或者一端查询多端的数据,以本场景为例,如果需要查询客户的所有记录,那么只需要查询出该客户,由于客户和消息存在一对多的关系,那么当查询出客户记录,通过客户访问消息的时候,Hibernate就会进一步查询该客户的所有消息;
 @Test
  @Transactional(readOnly = true)
   public void testR() {
       // 懒加载过程:
       // 1.findById  只会查询Customer 和其他关联的立即加载
       Optional<Customer> customer = repository.findById(1L);
       System.out.println("=====================");
       // 由于输出, 会自动调用customer.toString()
       System.out.println(customer);
   }

而若要查询是消息的某个状态,改状态不绑定某个客户,此时则可以通过多端的Message实体进行查询;

  1. 在多端通过命名接口的方式以外键作为查询条件的时候,接口的名称应该使用外键对应的实体名称,并且入参应该应用外捡实体类,但可用的查询条件仅为主键ID,如:
public interface MessageRepository extends PagingAndSortingRepository<Message, Long> {
   // 根据客户id查询所有信息
   // 通过规定方法名来实现关联查询: 需要通过关联属性来进行匹配
   // 但是只能通过id来进行匹配
   List<Message> findByCustomer(Customer customer);
}

2.3 多对多关系

场景:一个客户可以有多个角色,一个角色可以赋给多个客户

@Entity     // 作为hibernate 实体类
@Table(name = "tb_customer")       // 映射的表明
@Data
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long custId; //客户的主键
    @Column(name = "cust_name")
    private String custName;//客户名称
    @Column(name = "cust_address")
    private String custAddress;//客户地址

    // 单向多对多
    @ManyToMany(cascade = CascadeType.ALL)
    /*中间表需要通过@JoinTable来维护外键:(不设置也会自动生成)
     * name 指定中间表的名称
     * joinColumns 设置本表的外键名称
     * inverseJoinColumns 设置关联表的外键名称
     * */
    @JoinTable(
            name = "tb_customer_role",
            joinColumns = {@JoinColumn(name = "c_id")},
            inverseJoinColumns = {@JoinColumn(name = "r_id")}
    )
    private List<Role> roles;
}
@Entity
@Table(name="tb_role")
@Data
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name="role_name")
    private String rName;

    public Role(String rName) {
        this.rName = rName;
    }

    public Role(Long id, String rName) {
        this.id = id;
        this.rName = rName;
    }

    public Role() {
    }

    @ManyToMany(cascade = CascadeType.ALL)
    private List<Role> roles;
}
  1. 多对多同样存在单向多对多,以及双向多对多的配置;
  2. @JoinTable注解,由于@JoinColumn注解只能用于一对一、一对多、多对一映射关系中指定一个外键名称,而多对多映射关系会产生一张中间表,中间表会存在两张多对多映射关系表的外键约束,因此衍生出@JoinTable注解,可以指定中间表的名称以及两个外键的名称;
  3. 注意多对多不适用于级联删除,即如果应用@ManyToMany注解的cascade属性,可能会导致Hibernate抛出ConstraintViolationException,
/ *
  * 注意加上
  * @Transactional
    @Commit
    多对多其实不适合删除, 因为经常出现数据出现可能除了和当前这端关联还会关联另一端,此时删除就会: ConstraintViolationException。
    * 要删除, 要保证没有额外其他另一端数据关联
   * */
   @Test
   @Transactional
   @Commit
   public void testD() {
       Optional<Customer> customer = repository.findById(14L);
       repository.delete(customer.get());
   }

如下图所示,采用级联操作,当要删除c_id为14的值得时候,要级联删除r_id为9和10的记录,但这两条记录却被c_id为9的记录关联着,导致无法正常级联删除:


中间表映射关系

2.4 补充

不要试图直接在代码中创建一个ID存在于数据库的对象,然后寄希望于Hibernate会将该对象的修改应用到数据库中,这样创建出来的对象是游离态DETACH的,Hibernate无法对其进行管理,如果需要令Hibernate托管一个对象,需要通过Repository从数据库中查询出该对象。

/**
 * 错误例子
 */
@Test
@Transactional
@Commit
public void testC() {
    List<Role> roles = new ArrayList<>();
    roles.add(new Role(9L, "超级管理员"));
    roles.add(new Role(10L, "商品管理员"));

    Customer customer = new Customer();
    customer.setCustName("诸葛");
    customer.setRoles(roles);
    repository.save(customer);
}
/**
  * 正确例子
  * 1. 如果希望保存已有的关联数据 ,就需要从数据库中查出来(持久状态)。否则 提示游离状态不能持久化;
  * 2. 如果一个业务方法有多个持久化操作, 记得加上@Transactional,否则不能共用一个session;
  * 3. 在单元测试中用到了@Transactional , 如果有增删改的操作一定要加@Commit
  * 4. 单元测试会认为你的事务方法@Transactional,只用于测试,不会提交事务,从而需要单独加上@Commit
  */
@Test
@Transactional
@Commit
public void testC() {
    List<Role> roles = new ArrayList<>();
    roles.add(roleRepository.findById(9L).get());
    roles.add(roleRepository.findById(10L).get());

    Customer customer = new Customer();
    customer.setCustName("诸葛");
    customer.setRoles(roles);
    repository.save(customer);
}

3. 乐观锁

可以在实体类的属性上使用@Version标注,Hibernate将会对当前实体类对应的表创建一个version字段,并应用乐观锁机制进行数据修改操作。

@Entity     // 作为hibernate 实体类
@Table(name = "tb_customer")       // 映射的表明
@Data
public class Customer {
    @Version
    private Long version;
}

4. 操作人审计

  1. 添加如下依赖
<!--spring-test -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.10</version>
    <scope>test</scope>
</dependency>
  1. 添加配置类,启用审计功能@EnableJpaAuditing,注册返回当前操作用户的Bean对象AuditorAware<T>
@Configuration          // 标记当前类为配置类   =xml配文件
// 启动JPA的Repository扫描,就是XML配置中的<jpa:repositories ...>标签
@EnableJpaRepositories(basePackages="com.tuling.repositories")  
@EnableTransactionManagement  // 开启事务
@EnableJpaAuditing            // 开启JPA审计功能
public class SpringDataJPAConfig {

    /** 
     *  数据源
     */
    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/springdata_jpa?characterEncoding=UTF-8");
        return  dataSource;
    }

    /** 
     * EntityManagerFactory
     */
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
        vendorAdapter.setShowSql(true);
        LocalContainerEntityManagerFactoryBean factory 
                                = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.tuling.pojo");
        factory.setDataSource(dataSource());
        return factory;
    }

    /**
     * 事务管理器
     */
    @Bean
    public PlatformTransactionManager transactionManager(
                                                EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

    // AuditorAware 返回当前用户,泛型中的类型,根据实体类中的操作人类型决定
    @Bean
    public AuditorAware<String> auditorAware(){
        return new AuditorAware() {
            @Override
            public Optional getCurrentAuditor() {
                // 返回当前用户,可以从Session、redis等存储介质中获取当前用户
                return Optional.of("xushu");
            }
        };
    }
}
  1. 在实体类中新增添加人、添加时间、修改人、修改时间相关属性和注解,并在实体类上增加启用审计功能的注解@EntityListeners(AuditingEntityListener.class),注意这里创建人和修改人的类型,需要和上面注册类中返回的AuditorAware<T>泛型一致;
@Entity     // 作为hibernate 实体类
@Table(name = "tb_customer")       // 映射的表明
@Data
@EntityListeners(AuditingEntityListener.class)
public class Customer {

   // ...
   
   /**
    * 实体创建人
    */ 
   @CreatedBy
   String createdBy;
   /**
    * 实体创建时间
    */
   @Temporal(TemporalType.TIMESTAMP)
   @CreatedDate
   protected Date dateCreated = new Date();
   /**
    * 实体最后修改人
    */ 
   @LastModifiedBy
   String modifiedBy;
   /**
    * 实体修改时间
    */
   @Temporal(TemporalType.TIMESTAMP)
   @LastModifiedDate
   protected Date dateModified = new Date();
}
  1. 经过上述步骤,每次新增、修改记录时,SpringData-JPA将会在数据库表内维护如下字段:


    操作人审计数据库结构
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容

  • 本节重点:1、hibernate映射文件2、hibernate核心配置文件3、hibernate核心类 1、hib...
    Vincilovfang阅读 620评论 0 2
  • JPA 概述 Java Persistence API(Java 持久层 API):用于对象持久化的 API 作用...
    熊少文阅读 650评论 0 0
  • 2017年8月21日 我原本只想简单记录一下springboot中应用Jpa的简单操作。不想由于hibernate...
    行者N阅读 6,488评论 0 23
  • 最近学习hibernate注解形式配置POJO类,将注解的解析记下来,以备以后使用。 例1. Hibernate ...
    光剑书架上的书阅读 1,268评论 0 7
  • Hibernate 序 创建 hibernate 工程示例: 创建工程,引入 jar 包 创建配置文件 hiber...
    WJunF阅读 968评论 0 3