spring-data-jpa

Spring Data JPA学习

使用全注解方式,通过分析真正执行的SQL 来看注解的作用,
以及简单的分析一下源码,
使用p6spy 拦截使用的sql,专门记录的一个文件中,这样方便分析,
使用h2 的内存模式

环境

上述环境的配置,是基于 spring-boot

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>1.5.4.RELEASE</version>
  5. </parent>
  6. <dependencies>
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-web</artifactId>
  10. </dependency>
  11. <!-- Runtime -->
  12. <dependency>
  13. <groupId>com.h2database</groupId>
  14. <artifactId>h2</artifactId>
  15. <scope>runtime</scope>
  16. </dependency>
  17. <dependency>
  18. <groupId>p6spy</groupId>
  19. <artifactId>p6spy</artifactId>
  20. <version>3.0.0</version>
  21. </dependency>
  22. <!-- Test -->
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-test</artifactId>
  26. <scope>test</scope>
  27. </dependency>
  28. <!-- Compile -->
  29. <dependency>
  30. <groupId>org.springframework.boot</groupId>
  31. <artifactId>spring-boot-starter-data-jpa</artifactId>
  32. </dependency>
  33. <dependency>
  34. <groupId>org.hibernate</groupId>
  35. <artifactId>hibernate-java8</artifactId>
  36. </dependency>
  37. <dependency>
  38. <groupId>org.projectlombok</groupId>
  39. <artifactId>lombok</artifactId>
  40. <scope>provided</scope>
  41. </dependency>
  42. </dependencies>

基于spring-boot的配置就不需要多说了,依赖 h2 ,这是一个内嵌形式的数据库,通常可以在开发的时候使用这个数据库,在打包出去后,是不带的,正式运行可以通过修改数据库的配置参数连接正常的数据库 譬如 mysql, 'oracle' 等等,通常只需要修改连接和用户名密码。
hibernate-java8,这个包用来支持 java8 里面的时间日期 API, java8 自带的时间日期 API 还是挺好用的。
p6spy 就是就是一层数据库驱动的拦截,如果要使用这个,需要在数据库配置参数,将数据库驱动改为这个包里面的驱动,然后在spy.properties 配置文件中加上要代理的驱动类列表,还有一点要注意,就是 数据库连接的 url 需要修改下,在原来的基础上 jdbc:mysql... 中间加上 p6spy, jdbc:p6spy:mysql...
lombok 这个包可以让我们少些重复代码,可以通过注解在生成 getter setter equalsAndHashCode 之类的,具体用法可以看官网。本人认为还是挺方便的。

然后配置下 h2p6spy
在application.properties 中添加下面的配置

  1. spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver
  2. spring.datasource.url=jdbc:p6spy:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE

resources 目录下添加 spy.properties

  1. # module 这些模块的日志才会打印出来
  2. modulelist=com.p6spy.engine.spy.P6SpyFactory,com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
  3. # 被代理的驱动类列表
  4. driverlist=org.h2.Driver
  5. # 自动刷新,就是每拦截一条sql就写到日志中
  6. autoflush =false
  7. # 日期格式 yyyy-MM-dd HH:mm:ss
  8. dateformat=
  9. # 打印每条sql 的堆栈
  10. stacktrace=false
  11. # 上面的配置为true,下面的列表中的类的堆栈才打印
  12. stacktraceclass=
  13. # 是否自己加载配置文件
  14. reloadproperties=false
  15. # 加载配置文件间隔,单位秒s,如果上面的配置为true
  16. reloadpropertiesinterval=60
  17. # appender 日志记录的位置
  18. #appender=com.p6spy.engine.spy.appender.Slf4JLogger
  19. #appender=com.p6spy.engine.spy.appender.StdoutLogger
  20. appender=com.p6spy.engine.spy.appender.FileLogger
  21. # 日志名字,只用在FileLogger
  22. logfile = spy.log
  23. # 追加日志
  24. append=false
  25. # 日志信息格式
  26. logMessageFormat=com.p6spy.engine.spy.appender.SingleLineFormat
  27. # 数据库方言日期格式
  28. databaseDialectDateFormat=yyyy-MM-dd
  29. # 配置参数到JMX
  30. jmx=true
  31. # JMX 的前缀 默认为null ,com.p6spy(.<jmxPrefix>)?:name=<optionsClassName>
  32. #jmxPrefix=
  33. #
  34. #useNanoTime=false
  35. # 实际的数据库连接池,默认是spy的连接池,这两个配置,一旦配置了,不会受配置文件的reload影响
  36. #realdatasource=/RealMySqlDS
  37. #realdatasourceclass=com.mysql.jdbc.jdbc2.optional.MysqlDataSource
  38. # 数据库连接池需要的配置信息 key;value,key;value
  39. #realdatasourceproperties=port;3306,serverName;myhost,databaseName;jbossdb,foo;bar
  40. # JNDI 方式配置数据库连接池
  41. #jndicontextfactory=org.jnp.interfaces.NamingContextFactory
  42. #jndicontextproviderurl=localhost:1099
  43. #jndicontextcustom=java.naming.factory.url.pkgs;org.jboss.nameing:org.jnp.interfaces
  44. #jndicontextfactory=com.ibm.websphere.naming.WsnInitialContextFactory
  45. #jndicontextproviderurl=iiop://localhost:900
  46. # 是否开启日志拦截 include/exclude/sqlexpression 这3个配置受影响
  47. #filter=false
  48. # 满足关键字
  49. #include=
  50. # 排除关键字
  51. #exclude =
  52. # 正则表达式
  53. #sqlexpression =
  54. # 日志排除的类别 所有类别:error, info, batch, debug, statement, commit, rollback, result and resultset
  55. excludecategories=info,debug,result,batch
  56. # 二进制内容是否使用占位符记录
  57. #excludebinary=false
  58. # sql记录门槛 超过时间的sql才会被记录,默认是0,单位毫秒ms 类似慢查询日志
  59. #executionThreshold=

spring-boot 的启动类

  1. @SpringBootApplication
  2. @EnableJpaAuditing
  3. publicclassApplication{
  4. publicstaticvoid main(String[] args){
  5. SpringApplication.run(Application.class, args);
  6. }
  7. }

基础注解

学习数据库肯定要建表的,
学习过程中需要建立的表是根据用户,角色和权限来的,
每个表都的主键名都是 ID, 字符串类型,与业务无关,并且都有插入时间和上次修改时间字段,最好还有一个 version 字段,用来实现乐观锁机制(就是在修改操作的是 带条件 version= 你预期的)

来个hello world

使用JPA 不需要预先在数据库里面建表,只需要定义好实体类,就可以自动创建表了,
另外,使用了 h2 的内存模式,所以,也不需要本机装有什么数据库。

首先根据上述需求,创建一个 BaseEntity

  1. @MappedSuperclass
  2. @Data
  3. @NoArgsConstructor
  4. @EntityListeners({AuditingEntityListener.class})
  5. publicclassBaseEntity{
  6. /**
  7. * ID 主键
  8. */
  9. @Id
  10. @Column(name ="id", length =40)
  11. privateString id;
  12. /**
  13. * 如果要用 @Version 注解 ,插入的时候必须要有个值,这个可以设置为 not null 并加上一个默认值,这样以后更新就会顺带更新这个值
  14. * 如果刚插入的时候为null,后面会出问题的 save方法 如果是更新的话,会出现异常
  15. */
  16. @Version
  17. @Column(name ="version", nullable=false)
  18. @ColumnDefault("1")
  19. privateInteger version;
  20. /**
  21. * 使用 @CreatedDate @LastModifiedDate 注解 记得要在配置类上使用 @EnableJpaAuditing 开启这个功能
  22. *
  23. */
  24. @Column(name ="create_time", nullable =false)
  25. @CreatedDate
  26. privateLocalDateTime createTime;
  27. @Column(name ="last_operate_time", nullable =false)
  28. @LastModifiedDate
  29. privateLocalDateTime lastOperateTime;
  30. @Column(name ="valid", nullable =false)
  31. @ColumnDefault("true")
  32. privateBoolean valid;
  33. }

先不用管上面的细节, 然后通过继承这个类,来实现 用户表 的 实体类的

  1. @Entity
  2. @Table(name ="userinfo")
  3. @Data
  4. @NoArgsConstructor
  5. @EqualsAndHashCode(callSuper =true)
  6. publicclassUserInfoextendsBaseEntity{
  7. @Column(name ="username", length =40)
  8. privateString username;
  9. @Column(name ="age")
  10. privateInteger age;
  11. @Column(name ="phone")
  12. privateString phone;
  13. }

resources 目录下创建 import.sql 文件 ,这个文件会在spring-boot 项目启动后执行里面的 sql 语句

  1. INSERT INTO userinfo(id, create_time, last_operate_time, username, age, phone) VALUES('1','2017-07-21T20:20:00','2017-07-21T20:20:00','luolei',23,'12345678912');

最后,测试一下,让程序启动和停止,看中间会发生什么
写一下测试代码, 测试类上加上注解

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. @ActiveProfiles("scratch")

写一个空的测试方法,看会发生什么事情

  1. /**
  2. * 什么都不做,测试spring boot 项目启动 和关闭 时候执行情况
  3. */
  4. @Test
  5. publicvoid testNothing(){
  6. }

spring-boot 的启动那些日志我们不去管,我们值关心 JPA 相关的,可以看到在项目目录下生成了一个spy.log 文件,里面是执行的sql语句,把其中的sql语句复制出来,美化一下格式

  1. --1启动的时候,创建表前先删除之前存在的表,这个可以有个配置控制,可以每次启动都创建表然后新建,或者只是更新现有表,或者用以前的表
  2. --我们使用的是内存默认的数据库,因此可以先删除后新建,实际上每次启动都没有表
  3. DROP TABLE userinfo
  4. IF EXISTS
  5. --2创建表
  6. CREATE TABLE userinfo (
  7. id VARCHAR (40) NOT NULL,
  8. create_time TIMESTAMP NOT NULL,
  9. last_operate_time TIMESTAMP NOT NULL,
  10. valid boolean DEFAULT TRUE NOT NULL,
  11. version INTEGER DEFAULT 1 NOT NULL,
  12. age INTEGER,
  13. phone VARCHAR (255),
  14. username VARCHAR (40),
  15. PRIMARY KEY (id)
  16. )
  17. --3这个插入语句是import.sql 里面的
  18. INSERT INTO userinfo (
  19. id,
  20. create_time,
  21. last_operate_time,
  22. username,
  23. age,
  24. phone
  25. )
  26. VALUES
  27. (
  28. '1',
  29. '2017-07-21T20:20:00',
  30. '2017-07-21T20:20:00',
  31. 'luolei',
  32. 23,
  33. '15972991729'
  34. )
  35. --4程序结束,删除表
  36. DROP TABLE userinfo
  37. IF EXISTS

可以看到,sql语句的参数全部都是显示出来的,而且所有sql都是在一个文件中,这就是我目前想要的情况,
这个也有不足,譬如,没有时间,这个可以通过 修改 spy.properties 的appender ,改为 logger 形式的就行了,具体配置可以自行了解
就算加上了时间,但是我们可能需要知道这条sql执行时候的业务上下文,这个就没办法了,
这种情况下,可以通过配置 hibernate 的日志级别来满足,在 src/test/resource 下建立 application-scratch.properteis ,里面加上

  1. spring.jpa.show-sql=true
  2. #logging.level.org.hibernate.SQL=trace
  3. #为了显示参数
  4. #logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace
  5. #logger.level.hibernate.type.descriptor.sql.BasicExtractor=trace
  6. #查看查询中命名参数的值
  7. #logger.level.org.hibernate.engine.QueryParameters=debug
  8. #logger.level.org.hibernate.engine.query.HQLQueryPlan=debug

把注释掉的参数放出来就可以在应用日志中看到参数的信息了

@Entity

标记一个类为 数据库实体类
里面有一个属性为 name
这个是可选属性,配置实体类的名字,默认是实体类的类名,大部分时候不需要配置

@Table

用来定义一些表的信息

  • name
    表名,可以不填,默认是实体类名
  • catalog
    表的catalog,表属于那个数据库实例,可以不填,默认就是url里面指定的数据库
  • schema
    同上
  • uniqueConstraints
    定义唯一索引,主要用来定义多字段的唯一索引,单个字段的唯一索引可以在 @Column 中定义
  • indexes
    定义普通索引

@MappedSuperclass

这个注解没有属性,标记一个类为其他实体类的基类,定义一些公共字段用的

@Column

表明字段对应数据库表中的列,有以下属性

  • name
    列名,不填,默认就是字段名

  • unique
    是否唯一,默认false,如果是true,则会添加一个唯一索引

  • nullable
    是否可以为 null

  • insertable
    该列是否可插入,默认是true,如果是false,那个这列可能都是默认值

  • updatable
    是否可更新,默认true,譬如create_time 这列,在插入后就不应该更新

  • columnDefinition
    列备注,会在 建表sql中显示

  • table
    这个字段所属的表,一般不填这个属性把,默认就是主表,也就是这个类对应的表

  • length
    字段长度,主要针对 String 类型的

  • precision
    针对 decimal 类型来的

  • scale
    针对 decimal 类型来的

@ColumnDefault

这个不是JPA的注解,是hibernate的,添加字段的默认值

@Id

标记该字段为主键,没有属性,如果字段名不是跟列名相同,可以再添加上面的 @Column 注解

@Embeddable

用来类上,用法
譬如,联合主键,定义一个类 EmployeePK ,里面是联合主键的字段,然后在类 Employee 的一个字段类型是 EmployeePK,在字段上添加注解 @EmbeddedId, 标记为联合主键。
或者,譬如一些表有一些公共字段,不想在每个实体类里面重复定义,只需要定义一个包含公共字段的类,标记这个注解,然后在实体类中用这个类。

@Embedded

用在方法或者属性上的,标记使用这个属性类里面字段当表的列,跟上面的注解作用差不多,如果一个类上面没有标记 上面那个 @Embeddable 注解,那么,可以在该字段上标记本注解实现同样的效果

@GeneratedValue

用来指定主键生成策略,有两个属性

  • GenerationType
    生成策略,有4个选项 AUTO, TABLE, SEQUENCE, IDENTITY
    默认就是 AUTO, 代表交给 hibernate来从后面三个中选择,hibernate会根据使用的是什么数据库来选择,
    例如 MySQL 使用的是 IDENTITY 这个必须是数字,而 ORACLE 是 SEQUENCE,这个是字符串。
    显然,这样的主键生成是跟使用的数据库相关的,那能不能自定义呢,主键用字符串,用自己的生成策略。
  • generator
    生成器的名字
    通常用来指定自定义主键生成策略

我们修改下 BaseEntity 的主键那部分

  1. /**
  2. * ID 主键
  3. */
  4. @Id
  5. @GeneratedValue(generator ="idGen")
  6. @GenericGenerator(name ="idGen", strategy ="com.luolei.springdata.jpa.util.KeyUtils",
  7. parameters ={@Parameter(name ="dataCenterID", value ="d1"),@Parameter(name ="idLength", value ="10")})
  8. @Column(name ="id", length =40)
  9. privateString id;

看下自定义的主键生成类

  1. publicclassKeyUtilsextendsAbstractUUIDGeneratorimplementsConfigurable{
  2. // 数据中心ID
  3. privateString dataCenterID;
  4. //主键长度
  5. privateint idLength;
  6. privateAtomicInteger adder =newAtomicInteger(0);
  7. @Override
  8. publicSerializable generate(SessionImplementor session,Object object)throwsHibernateException{
  9. long timestamp =System.currentTimeMillis();
  10. String id = dataCenterID + timestamp + adder.getAndIncrement();
  11. if(adder.get()>99){
  12. adder.set(0);
  13. }
  14. return id;
  15. }
  16. @Override
  17. publicvoid configure(Type type,Properties params,ServiceRegistry serviceRegistry)throwsMappingException{
  18. this.dataCenterID = params.getProperty("dataCenterID","default");
  19. try{
  20. idLength =Integer.parseInt(params.getProperty("idLength","8"));
  21. }catch(NumberFormatException e){
  22. idLength =8;
  23. }
  24. }
  25. }

使用这个配置,来测试上面的测试,将设置id那行注释掉,查看执行的sql,

  1. INSERT INTO t_idcard (
  2. create_time,
  3. last_operate_time,
  4. version,
  5. address,
  6. card_no,
  7. id
  8. )
  9. VALUES
  10. (
  11. '2017-07-25',
  12. '2017-07-25',
  13. 0,
  14. 'AD',
  15. '12',
  16. 'd115009536083520'
  17. )
  18. INSERT INTO userinfo (
  19. create_time,
  20. last_operate_time,
  21. version,
  22. age,
  23. card_id,
  24. phone,
  25. username,
  26. id
  27. )
  28. VALUES
  29. (
  30. '2017-07-25',
  31. '2017-07-25',
  32. 0,
  33. 12,
  34. 'd115009536083520',
  35. '123',
  36. 'username2',
  37. 'd115009536082770'
  38. )

我们可以看到自己生成的 插入身份记录的 主键 就是使用我们自定义生成的策略
需要注意的一点,一旦指定了主键生成策略,无论是自定义的,还是系统策略,这时候在自己主动设置主键ID,都是不生效的,也就是在目前这个策略下,就算自己设置 card的主键id 为 ‘2’,也是没作用的。

关联关系和级联级别

@OneToOne

一对一关联关系,通常是一张表有外键,当然也可以两张表都有外键,以用户和身份证两个表为例,
显然用户和身份证是一一对应的关系,来试一下

  1. // 身份证实体类长这样
  2. @Data
  3. @NoArgsConstructor
  4. @EqualsAndHashCode(callSuper =true)
  5. @Entity
  6. @Table(name ="t_idcard")
  7. publicclassIDCardextendsBaseEntity{
  8. @Column(name ="card_no", length =20, nullable =false, unique =true)
  9. privateString cardNo;
  10. @Column(name ="address", length =40)
  11. privateString address;
  12. }
  13. //用户信息实体类长这样
  14. @Entity
  15. @Table(name ="userinfo", indexes ={@Index(columnList ="phone")}, uniqueConstraints ={@UniqueConstraint(columnNames ={"username"}),@UniqueConstraint(columnNames ={"phone"})})
  16. @Data
  17. @NoArgsConstructor
  18. @EqualsAndHashCode(callSuper =true)
  19. publicclassUserInfoextendsBaseEntity{
  20. @Column(name ="username", length =40)
  21. privateString username;
  22. @Column(name ="age")
  23. privateInteger age;
  24. @Column(name ="phone")
  25. privateString phone;
  26. @OneToOne
  27. privateIDCard idCard;
  28. }

然后运行一下空的测试方法,查看建表语句

  1. CREATE TABLE t_idcard (
  2. id VARCHAR (40) NOT NULL,
  3. create_time TIMESTAMP NOT NULL,
  4. last_operate_time TIMESTAMP NOT NULL,
  5. valid boolean DEFAULT TRUE NOT NULL,
  6. version INTEGER DEFAULT 1 NOT NULL,
  7. address VARCHAR (40),
  8. card_no VARCHAR (20) NOT NULL,
  9. PRIMARY KEY (id)
  10. )
  11. CREATE TABLE userinfo (
  12. id VARCHAR (40) NOT NULL,
  13. create_time TIMESTAMP NOT NULL,
  14. last_operate_time TIMESTAMP NOT NULL,
  15. valid boolean DEFAULT TRUE NOT NULL,
  16. version INTEGER DEFAULT 1 NOT NULL,
  17. age INTEGER,
  18. phone VARCHAR (255),
  19. username VARCHAR (40),
  20. --关注下面这个字段,这是自动创建的,命名规则为字段名_关联表的主键名
  21. id_card_id VARCHAR (40),
  22. PRIMARY KEY (id)
  23. )
  24. alter table t_idcard add constraint UK_okqcwtcdgbdm0meqgqhifckk9 unique (card_no)
  25. reate index IDXnijxoy2hk6npm7lweieo2w56j on userinfo (phone)
  26. alter table userinfo add constraint UK8h620irpir8kcurgsdkhns8lt unique (username)
  27. alter table userinfo add constraint UKnijxoy2hk6npm7lweieo2w56j unique (phone)
  28. --这里,添加了一个外键
  29. alter table userinfo add constraint FK7tk1cso93kbopcvt2mj5yhoru foreign key (id_card_id) references t_idcard

主要注意有 SQL 中有注释的地方,在 @OneToOne 注解后还可以添加 @JoinColumn 注解,来自己明确指定列名,就像下面这样

  1. @OneToOne
  2. @JoinColumn(name ="card_id")
  3. privateIDCard idCard;

来看下 @OneToOne 注解里面常用的属性,我们先做个简单的测试,在用户表和身份表加一条数据,并且关联

  1. INSERT INTO t_idcard(id, create_time, last_operate_time, card_no, address) VALUES('1','2017-07-21T20:20:00','2017-07-21T20:20:00','123456','address');
  2. INSERT INTO userinfo(id, create_time, last_operate_time, username, age, phone, card_id) VALUES('1','2017-07-21T20:20:00','2017-07-21T20:20:00','luolei',23,'12345678912','1');

然后单元测试一把

  1. /**
  2. * 我们在import.sql 里面插入了一条 idcard 和一条用户信息,并且关联了 一对一
  3. */
  4. @Test
  5. publicvoid testOrphanRemovalIsFalse(){
  6. assertThat(this.userInfoRepository.count()).isEqualTo(1L);
  7. assertThat(this.cardRepository.count()).isEqualTo(1L);
  8. this.userInfoRepository.delete("1");
  9. System.out.println("user count: "+this.userInfoRepository.count());
  10. System.out.println("card count: "+this.cardRepository.count());
  11. }

注意输出的count,当我们只写了 @OneToOne ,没有配置任何属性,那么user的记录数是0,card的记录数为1,
用户数据的删除,并不影响card表,这在大部分时候都满足需求,但是又是可能需要当用户表的数据删除后,身份信息也删除, 银行只有身份信息对系统来说没有任何左右,
这个时候需求就是当删除用户记录,级联删除身份信息。
查看 @OneToOne 的代码及注释信息,发现有一个属性为 orphanRemoval,注释的大概意思是会级联删除,默认是为 false的,我们设置为true测试看看

  1. @OneToOne(orphanRemoval =true)

SQL 语句

  1. --删除用户
  2. DELETE
  3. FROM
  4. userinfo
  5. WHERE
  6. id =?
  7. AND version =?| DELETE
  8. FROM
  9. userinfo
  10. WHERE
  11. id ='1'
  12. AND version =1
  13. --删除身份
  14. DELETE
  15. FROM
  16. t_idcard
  17. WHERE
  18. id =?
  19. AND version =?| DELETE
  20. FROM
  21. t_idcard
  22. WHERE
  23. id ='1'
  24. AND version =1

测试结果发现确实是级联删除了,
级联删除最好不要配置,因为删除不可控,如果需要删除,最好能够自己主动控制删除。
再看下其他属性,有个 CascadeType 这个也是设置级联级别的,级别有 ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH
目前我们只对级联删除感兴趣,就是remove,我们配置级联

  1. @OneToOne(cascade ={CascadeType.REMOVE})

经过测试发现,这个确实可以级联删除。
其他属性:

  • fetch
    获取方式,有懒加载和立即加载,默认是立即,如果设置为懒加载又是可能有出问题,因为正常情况下通过 session 操作数据库后,session 会关闭,这个时候再去访问就会异常了。
    如果不是性能特别敏感,或者要加载的字段里面不包含特别大的数据量,还是建议使用立即加载,简单粗暴。
  • optional
    可选,默认是true,就是这个外键是否允许为null,这个根据需要填写
  • mapperBy
    这个属性,发现怎么填都是错误的,还是不管这个属性把
  • targetEntity
    这个会自动帮你处理的,一般情况不需要管他

总的来说,一对一关系还是非常简单的,外键可以在两个表的任意一个表上,或者两个表互相都有外键,
正常情况下使用注解 @OneToOne@JoinColumn 注解就行了,也不需要特别的配置。

上面说了级联删除 CascadeType.REMOTE, 在尝试下级联插入 CascadeType.PERSIST
代码如下

  1. @OneToOne(cascade ={CascadeType.REMOVE,CascadeType.PERSIST})
  2. @JoinColumn(name ="card_id")
  3. privateIDCard idCard;

测试代码,先各自插入一条记录

  1. @Test
  2. publicvoid testOneToOne01(){
  3. //初始插入了一条 身份信息 和 一条用户信息
  4. assertThat(this.userRepository.count()).isEqualTo(1L);
  5. assertThat(this.cardRepository.count()).isEqualTo(1L);
  6. UserInfo userInfo =newUserInfo();
  7. userInfo.setUsername("username2");
  8. userInfo.setPhone("123");
  9. userInfo.setAge(12);
  10. userInfo.setId("2");
  11. IDCard card =newIDCard();
  12. card.setCardNo("12");
  13. card.setAddress("AD");
  14. card.setId("2");
  15. userInfo.setIdCard(card);
  16. this.userRepository.save(userInfo);
  17. assertThat(this.userRepository.count()).isEqualTo(2L);
  18. assertThat(this.cardRepository.count()).isEqualTo(2L);
  19. }

可以看到测试是成功的,查下下SQL 的执行情况

  1. INSERT INTO t_idcard (
  2. create_time,
  3. last_operate_time,
  4. version,
  5. address,
  6. card_no,
  7. id
  8. )
  9. VALUES
  10. (
  11. '2017-07-25',
  12. '2017-07-25',
  13. 0,
  14. 'AD',
  15. '12',
  16. '2'
  17. )
  18. INSERT INTO userinfo (
  19. create_time,
  20. last_operate_time,
  21. version,
  22. age,
  23. card_id,
  24. phone,
  25. username,
  26. id
  27. )
  28. VALUES
  29. (
  30. '2017-07-25',
  31. '2017-07-25',
  32. 0,
  33. 12,
  34. '2',
  35. '123',
  36. 'username2',
  37. '2'
  38. )

先插入了一条 身份信息,然后插入了一条用户信息,所以两个表的记录都是2条,
我们再试一下不配置级联插入,测试一下。
你可以发现出现异常了,因为不会级联插入,就不会先插入身份信息,但是插入用户信息的时候有身份信息的ID,是外键关联,但是身份表没有这个字段,所以包错。
通过上面这个例子就能知道级联插入的作用了。
我们继续测试,配置级联插入,但是不设置 身份的主键ID

  1. @Test
  2. publicvoid testOneToOne01(){
  3. //初始插入了一条 身份信息 和 一条用户信息
  4. assertThat(this.userRepository.count()).isEqualTo(1L);
  5. assertThat(this.cardRepository.count()).isEqualTo(1L);
  6. UserInfo userInfo =newUserInfo();
  7. userInfo.setUsername("username2");
  8. userInfo.setPhone("123");
  9. userInfo.setAge(12);
  10. userInfo.setId("2");
  11. IDCard card =newIDCard();
  12. card.setCardNo("12");
  13. card.setAddress("AD");
  14. // card.setId("2");
  15. userInfo.setIdCard(card);
  16. this.userRepository.save(userInfo);
  17. assertThat(this.userRepository.count()).isEqualTo(2L);
  18. assertThat(this.cardRepository.count()).isEqualTo(2L);
  19. }

我们会发现,程序还是尝试先级联插入身份记录,但是没有给主键赋值,因此是错误的。
所有要知道级联插入的用法,一旦设置级联插入,当本实体类有其他实体引用,并且要保存的时候,其他实体一定要有他自己的主键,否则会报错。
级联插入还有一个问题,就是当这个引用的实体是从数据库查询出来的,要插入的实体是新建的,这个时候进行插入,还是可能会出现异常。
所以还是尽量不要配置级联删除。

这就会有个问题,一般主键都是业务无关,每次主键都要由应用内设置在保存,显然也不太方便,我们希望主键能按照我们的设想自动生成,例如自定义的全局唯一流水号,之类的。看上面的自定义主键生成策略

在来看下级联更新 CascadeType.MERGE
我们新做一个单元测试

  1. @Test
  2. publicvoid testCascadeMerge(){
  3. //初始插入了一条 身份信息 和 一条用户信息
  4. assertThat(this.userRepository.count()).isEqualTo(1L);
  5. assertThat(this.cardRepository.count()).isEqualTo(1L);
  6. UserInfo userInfo =this.userRepository.findOne("1");
  7. assertThat(userInfo).isNotNull();
  8. IDCard card = userInfo.getIdCard();
  9. assertThat(card).isNotNull();
  10. userInfo.setAge(44);//修改一下用户的信息,如果不修改用户的信息,就不会触发更新操作
  11. card.setAddress("hello world");//修改card的信息
  12. this.userRepository.save(userInfo);
  13. }

查看这次更新操作执行的SQL

  1. UPDATE userinfo
  2. SET create_time ='2017-07-21',
  3. last_operate_time ='2017-07-25',
  4. version =2,
  5. age =44,
  6. card_id ='1',
  7. phone ='12345678912',
  8. username ='luolei'
  9. WHERE
  10. id ='1'
  11. AND version =1

只更新了用户的信息,虽然我们在里面也修改了 身份信息,但是并没有触发更新
现在,配置一下级联更新

  1. @OneToOne(cascade ={CascadeType.REMOVE,CascadeType.PERSIST,CascadeType.MERGE})
  2. @JoinColumn(name ="card_id")
  3. privateIDCard idCard;

然后再次执行上面的测试,查看 SQL 语句

  1. UPDATE t_idcard
  2. SET create_time ='2017-07-21',
  3. last_operate_time ='2017-07-25',
  4. version =2,
  5. address ='hello world',
  6. card_no ='123456'
  7. WHERE
  8. id ='1'
  9. AND version =1
  10. UPDATE userinfo
  11. SET create_time ='2017-07-21',
  12. last_operate_time ='2017-07-25',
  13. version =2,
  14. age =44,
  15. card_id ='1',
  16. phone ='12345678912',
  17. username ='luolei'
  18. WHERE
  19. id ='1'
  20. AND version =1

这次可以看到先触发了身份信息的更新,然后才更新用户信息。
级联更新一般情况下可以配置,通常不会出现什么太大的问题。

级联刷新 CascadeType.REFRESH,
这个就是当获取用户的时候,也会尝试获取最新的身份信息,用的比较少

级联 CascadeType.DETACH
这个不知道是干啥的。。

总结一下,级联默认是没有的,要配置可以配置一下级联更新。

说完级联,再来说下 mapperBy

现在 用户实体长这样

  1. @Entity
  2. @Table(name ="userinfo")
  3. @Data
  4. @NoArgsConstructor
  5. @EqualsAndHashCode(callSuper =true)
  6. publicclassUserInfoextendsBaseEntity{
  7. @Column(name ="username", length =40)
  8. privateString username;
  9. @Column(name ="age")
  10. privateInteger age;
  11. @Column(name ="phone")
  12. privateString phone;
  13. @OneToOne(cascade ={CascadeType.REMOVE,CascadeType.PERSIST,CascadeType.MERGE,CascadeType.DETACH})
  14. @JoinColumn(name ="card_id")
  15. privateIDCard idCard;
  16. }

在里面有身份实体,而且外键在用户表上,列名为 card_id

在身份实体中,之前并没有引用用户实体。如果我想要引用怎么办呢?
一样的,在 IDCard里面加上一个 UserInfo字段,标记 @OneToOne 就行了,
但是这会出现一个问题,就是这是双向关联的,会在身份表上自动生成一个外键,
如果你的需求就是这样,那么在 IDCard 类 UserInfo字段上添加 @JoinColumn 注解自定义一下列名就行了
如果你只是想要一边有外键,只是想在代码中这样使用而已,你需要在 @OneToOne 注解上配置属性 mapperBy
代表维护外键的字段值,这个值是实体类里面的字段名,而不是列名,
例如,现在想外键在userinfo表维护,那么就应该在 IDCard 类那边添加这个 mapperBy ,值为 UserInfo 实体的字段名 idCard

@ManyToOne

多对一关系

这个关系也比较简单,通常是在多的一方有外键。例如 订单 和 订单明细 一个订单 Order 有多个订单明细 OrderItem
OrderItem 就是多的一方,通常是在OrderItem 里面有order的外键关联,正常使用,通常就是加一个 @ManyToOne 注解 加上一个 @JoinColumn 注解

  1. @Data
  2. @NoArgsConstructor
  3. @EqualsAndHashCode(callSuper =true)
  4. @Entity
  5. @Table(name ="t_order_item")
  6. publicclassOrderItemextendsBaseEntity{
  7. @Column(name ="item_id", length =20, nullable =false, unique =true)
  8. privateString item_ID;
  9. @Column(name ="product_id", length =20, nullable =false)
  10. privateString productID;
  11. @Column(name ="product_name", length =60)
  12. privateString productName;
  13. @Column(name ="price", precision =19, scale =2)
  14. privateBigDecimal price;
  15. @ManyToOne
  16. @JoinColumn(name ="order_id")
  17. privateOrder order;
  18. }

@ManyToOne 里面的属性就没什么好说的了
@JoinColumn 也没啥说的,就是定义关联的字段用的

@OneToMany

还是订单和订单明细的例子,通常,我们拿到订单的时候,都会想要知道订单里面的明细的,订单和明细是一个 一对多 的关系,
在代码层面上就是在Order 类里面有 OrderItem 的集合

  1. @Data
  2. @NoArgsConstructor
  3. @EqualsAndHashCode(callSuper =true)
  4. @Entity
  5. @Table(name ="t_order")
  6. publicclassOrderextendsBaseEntity{
  7. @Column(name ="order_id", length =40, unique =true, nullable =false)
  8. privateString orderID;
  9. @OneToMany(mappedBy ="order", fetch =FetchType.EAGER)
  10. privateList<OrderItem> items;
  11. }

需要注意的是,指定 mapperBy 属性,那么就会创建一个中间表,指定的值是Order实体类里面外键的字段名 而不是 列名 ,这个要注意。
还有就是默认的获取方式是 延时加载的,但是在web项目中,可能会出现问题,如果数据量不大,或者性能要求不是非常敏感,可以考虑立即加载
还有就是级联关系了,这个之前分析过了。

@ManyToMany

多对多的关系,这个通常很少。
但是也是有的,譬如角色Role 和 权限 Permission, 这个就是多对多的关系
多对多关系通常都会有一个中间表的,例如 role_permission ,
可以通过 @JoinTable 注解来控制

  1. @Entity
  2. @Table(name ="t_role")
  3. @Data
  4. @NoArgsConstructor
  5. @EqualsAndHashCode(callSuper =true)
  6. publicclassRoleextendsBaseEntity{
  7. @Column(name ="role_name", length =40, nullable =false, unique =true)
  8. privateString roleName;
  9. @Column(name ="role_desc", length =100)
  10. privateString roleDesc;
  11. @ManyToMany
  12. @JoinTable(name ="t_role_permission",
  13. joinColumns ={@JoinColumn(name ="role_id")},
  14. inverseJoinColumns ={@JoinColumn(name ="permission_id")},
  15. indexes ={@Index(columnList ="role_id")})
  16. privateSet<Permission> permissions;
  17. }

这个需要说的是,如果没有设置级联关系,新建 Role 里面添加 Permission 的时候,这些Permission 一定是要已经持久化的,否则,Role 保存的时候会出错。
还有当Role里面的Permssion 改变的时候,调用 save方法更新,会自动更新中间表的内容,同样也会自动删除。

对数据库连接信息进行加密

使用依赖

  1. <!-- 加解密配置文件里面 properties -->
  2. <dependency>
  3. <groupId>com.github.ulisesbocchio</groupId>
  4. <artifactId>jasypt-spring-boot-starter</artifactId>
  5. <version>1.14</version>
  6. </dependency>

然后在启动类上标记一下,启动这个功能

  1. @SpringBootApplication
  2. @EnableJpaAuditing
  3. @EnableEncryptableProperties
  4. publicclassApplication{
  5. publicstaticvoid main(String[] args){
  6. SpringApplication.run(Application.class, args);
  7. }
  8. }

添加需要配置的参数

  1. # salt 默认是随机的,随机生成的,等到下次启动应用就变了,我现在想要加密 数据库连接相关信息,肯定不允许改变的
  2. jasypt.encryptor.saltGeneratorClassname = org.jasypt.salt.ZeroSaltGenerator
  3. # 这个密码就只能明文了
  4. jasypt.encryptor.password =Ebnb$2017

单元测试一下,把需要加密的内容先加下密,然后放到配置文件中

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. publicclassEncryptTest{
  4. privatestaticLogger logger =LoggerFactory.getLogger(EncryptTest.class);
  5. @Autowired
  6. privateStringEncryptor encryptor;
  7. @Test
  8. publicvoid testEncrypt(){
  9. List<String> originStrs =Lists.newArrayList("com.p6spy.engine.spy.P6SpyDriver","jdbc:p6spy:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE","Ebnb$2017");
  10. logger.info("--------- 开始加密 ------------");
  11. List<String> encryptStrs = originStrs.stream()
  12. .map(str ->{
  13. String encryptStr = encryptor.encrypt(str);
  14. logger.info("{}:{}", str, encryptStr);
  15. return encryptStr;
  16. })
  17. .collect(Collectors.toList());
  18. logger.info("--------- 结束加密 ------------");
  19. logger.info("--------- 开始解密 ------------");
  20. encryptStrs.forEach(s -> logger.info("{}:{}", s, encryptor.decrypt(s)));
  21. logger.info("--------- 结束解密 ------------");
  22. }
  23. }

然后将加密过的密文放到配置文件中

  1. #spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver
  2. spring.datasource.driverClassName=ENC(5P1XZXp/AUnwWMSuv/RC5PNQk6Lmy5lSWvbULdWMESzOnCm0LL+SNA==)
  3. #spring.datasource.url=jdbc:p6spy:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE
  4. spring.datasource.url=ENC(HJpIIcYOhsGYVfMEvSn+6FwKgQKTethY2OJAC1iBbKieTi/BOHkwbMvw2jdu02Cn)

其中 使用 ENC() 包住的才是需要解密的配置。

posted on 2017-07-26 20:55  头机器人  阅读(573)  评论(0编辑  收藏  举报

导航