• 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() 包住的才是需要解密的配置。

  • 相关阅读:
    全局配置策略
    RESTful api介绍
    AJAX
    django cookie session 自定义分页
    mysql 索引优化
    yii2 response响应配置
    Django中的信号
    django orm相关操作
    django orm介绍以及字段和参数
    django form和ModelForm组件
  • 原文地址:https://www.cnblogs.com/luolei/p/7241900.html
Copyright © 2020-2023  润新知