zoukankan      html  css  js  c++  java
  • 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() 包住的才是需要解密的配置。

  • 相关阅读:
    hdu acm 2844 Coins 解题报告
    hdu 1963 Investment 解题报告
    codeforces 454B. Little Pony and Sort by Shift 解题报告
    广大暑假训练1 E题 Paid Roads(poj 3411) 解题报告
    hdu acm 2191 悼念512汶川大地震遇难同胞——珍惜现在,感恩生活
    hdu acm 1114 Piggy-Bank 解题报告
    poj 2531 Network Saboteur 解题报告
    数据库范式
    ngnix 配置CI框架 与 CI的简单使用
    Vundle的安装
  • 原文地址:https://www.cnblogs.com/luolei/p/7241900.html
Copyright © 2011-2022 走看看