环境
基础注解
来个hello world
@Entity
@Table
@MappedSuperclass
@Column
@ColumnDefault
@Id
@Embeddable
@Embedded
@GeneratedValue
关联关系和级联级别
@OneToOne
@ManyToOne
@OneToMany
@ManyToMany
对数据库连接信息进行加密
Spring Data JPA学习
使用全注解方式,通过分析真正执行的SQL 来看注解的作用,
以及简单的分析一下源码,
使用p6spy 拦截使用的sql,专门记录的一个文件中,这样方便分析,
使用h2 的内存模式
环境
上述环境的配置,是基于 spring-boot
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>1.5.4.RELEASE</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Runtime --><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency><dependency><groupId>p6spy</groupId><artifactId>p6spy</artifactId><version>3.0.0</version></dependency><!-- Test --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- Compile --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.hibernate</groupId><artifactId>hibernate-java8</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency></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 之类的,具体用法可以看官网。本人认为还是挺方便的。
然后配置下 h2 和 p6spy
在application.properties 中添加下面的配置
spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriverspring.datasource.url=jdbc:p6spy:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE
在resources 目录下添加 spy.properties
# module 这些模块的日志才会打印出来modulelist=com.p6spy.engine.spy.P6SpyFactory,com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory# 被代理的驱动类列表driverlist=org.h2.Driver# 自动刷新,就是每拦截一条sql就写到日志中autoflush =false# 日期格式 yyyy-MM-dd HH:mm:ssdateformat=# 打印每条sql 的堆栈stacktrace=false# 上面的配置为true,下面的列表中的类的堆栈才打印stacktraceclass=# 是否自己加载配置文件reloadproperties=false# 加载配置文件间隔,单位秒s,如果上面的配置为truereloadpropertiesinterval=60# appender 日志记录的位置#appender=com.p6spy.engine.spy.appender.Slf4JLogger#appender=com.p6spy.engine.spy.appender.StdoutLoggerappender=com.p6spy.engine.spy.appender.FileLogger# 日志名字,只用在FileLoggerlogfile = spy.log# 追加日志append=false# 日志信息格式logMessageFormat=com.p6spy.engine.spy.appender.SingleLineFormat# 数据库方言日期格式databaseDialectDateFormat=yyyy-MM-dd# 配置参数到JMXjmx=true# JMX 的前缀 默认为null ,com.p6spy(.<jmxPrefix>)?:name=<optionsClassName>#jmxPrefix=##useNanoTime=false# 实际的数据库连接池,默认是spy的连接池,这两个配置,一旦配置了,不会受配置文件的reload影响#realdatasource=/RealMySqlDS#realdatasourceclass=com.mysql.jdbc.jdbc2.optional.MysqlDataSource# 数据库连接池需要的配置信息 key;value,key;value#realdatasourceproperties=port;3306,serverName;myhost,databaseName;jbossdb,foo;bar# JNDI 方式配置数据库连接池#jndicontextfactory=org.jnp.interfaces.NamingContextFactory#jndicontextproviderurl=localhost:1099#jndicontextcustom=java.naming.factory.url.pkgs;org.jboss.nameing:org.jnp.interfaces#jndicontextfactory=com.ibm.websphere.naming.WsnInitialContextFactory#jndicontextproviderurl=iiop://localhost:900# 是否开启日志拦截 include/exclude/sqlexpression 这3个配置受影响#filter=false# 满足关键字#include=# 排除关键字#exclude =# 正则表达式#sqlexpression =# 日志排除的类别 所有类别:error, info, batch, debug, statement, commit, rollback, result and resultsetexcludecategories=info,debug,result,batch# 二进制内容是否使用占位符记录#excludebinary=false# sql记录门槛 超过时间的sql才会被记录,默认是0,单位毫秒ms 类似慢查询日志#executionThreshold=
spring-boot 的启动类
@SpringBootApplication@EnableJpaAuditingpublicclassApplication{publicstaticvoid main(String[] args){SpringApplication.run(Application.class, args);}}
基础注解
学习数据库肯定要建表的,
学习过程中需要建立的表是根据用户,角色和权限来的,
每个表都的主键名都是 ID, 字符串类型,与业务无关,并且都有插入时间和上次修改时间字段,最好还有一个 version 字段,用来实现乐观锁机制(就是在修改操作的是 带条件 version= 你预期的)
来个hello world
使用JPA 不需要预先在数据库里面建表,只需要定义好实体类,就可以自动创建表了,
另外,使用了 h2 的内存模式,所以,也不需要本机装有什么数据库。
首先根据上述需求,创建一个 BaseEntity
@MappedSuperclass@Data@NoArgsConstructor@EntityListeners({AuditingEntityListener.class})publicclassBaseEntity{/*** ID 主键*/@Id@Column(name ="id", length =40)privateString id;/*** 如果要用 @Version 注解 ,插入的时候必须要有个值,这个可以设置为 not null 并加上一个默认值,这样以后更新就会顺带更新这个值* 如果刚插入的时候为null,后面会出问题的 save方法 如果是更新的话,会出现异常*/@Version@Column(name ="version", nullable=false)@ColumnDefault("1")privateInteger version;/*** 使用 @CreatedDate @LastModifiedDate 注解 记得要在配置类上使用 @EnableJpaAuditing 开启这个功能**/@Column(name ="create_time", nullable =false)@CreatedDateprivateLocalDateTime createTime;@Column(name ="last_operate_time", nullable =false)@LastModifiedDateprivateLocalDateTime lastOperateTime;@Column(name ="valid", nullable =false)@ColumnDefault("true")privateBoolean valid;}
先不用管上面的细节, 然后通过继承这个类,来实现 用户表 的 实体类的
@Entity@Table(name ="userinfo")@Data@NoArgsConstructor@EqualsAndHashCode(callSuper =true)publicclassUserInfoextendsBaseEntity{@Column(name ="username", length =40)privateString username;@Column(name ="age")privateInteger age;@Column(name ="phone")privateString phone;}
在 resources 目录下创建 import.sql 文件 ,这个文件会在spring-boot 项目启动后执行里面的 sql 语句
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');
最后,测试一下,让程序启动和停止,看中间会发生什么
写一下测试代码, 测试类上加上注解
@RunWith(SpringRunner.class)@SpringBootTest@ActiveProfiles("scratch")
写一个空的测试方法,看会发生什么事情
/*** 什么都不做,测试spring boot 项目启动 和关闭 时候执行情况*/@Testpublicvoid testNothing(){}
spring-boot 的启动那些日志我们不去管,我们值关心 JPA 相关的,可以看到在项目目录下生成了一个spy.log 文件,里面是执行的sql语句,把其中的sql语句复制出来,美化一下格式
--1启动的时候,创建表前先删除之前存在的表,这个可以有个配置控制,可以每次启动都创建表然后新建,或者只是更新现有表,或者用以前的表--我们使用的是内存默认的数据库,因此可以先删除后新建,实际上每次启动都没有表DROP TABLE userinfoIF EXISTS--2创建表CREATE TABLE userinfo (id VARCHAR (40) NOT NULL,create_time TIMESTAMP NOT NULL,last_operate_time TIMESTAMP NOT NULL,valid boolean DEFAULT TRUE NOT NULL,version INTEGER DEFAULT 1 NOT NULL,age INTEGER,phone VARCHAR (255),username VARCHAR (40),PRIMARY KEY (id))--3这个插入语句是import.sql 里面的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,'15972991729')--4程序结束,删除表DROP TABLE userinfoIF EXISTS
可以看到,sql语句的参数全部都是显示出来的,而且所有sql都是在一个文件中,这就是我目前想要的情况,
这个也有不足,譬如,没有时间,这个可以通过 修改 spy.properties 的appender ,改为 logger 形式的就行了,具体配置可以自行了解
就算加上了时间,但是我们可能需要知道这条sql执行时候的业务上下文,这个就没办法了,
这种情况下,可以通过配置 hibernate 的日志级别来满足,在 src/test/resource 下建立 application-scratch.properteis ,里面加上
spring.jpa.show-sql=true#logging.level.org.hibernate.SQL=trace#为了显示参数#logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace#logger.level.hibernate.type.descriptor.sql.BasicExtractor=trace#查看查询中命名参数的值#logger.level.org.hibernate.engine.QueryParameters=debug#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 的主键那部分
/*** ID 主键*/@Id@GeneratedValue(generator ="idGen")@GenericGenerator(name ="idGen", strategy ="com.luolei.springdata.jpa.util.KeyUtils",parameters ={@Parameter(name ="dataCenterID", value ="d1"),@Parameter(name ="idLength", value ="10")})@Column(name ="id", length =40)privateString id;
看下自定义的主键生成类
publicclassKeyUtilsextendsAbstractUUIDGeneratorimplementsConfigurable{// 数据中心IDprivateString dataCenterID;//主键长度privateint idLength;privateAtomicInteger adder =newAtomicInteger(0);@OverridepublicSerializable generate(SessionImplementor session,Object object)throwsHibernateException{long timestamp =System.currentTimeMillis();String id = dataCenterID + timestamp + adder.getAndIncrement();if(adder.get()>99){adder.set(0);}return id;}@Overridepublicvoid configure(Type type,Properties params,ServiceRegistry serviceRegistry)throwsMappingException{this.dataCenterID = params.getProperty("dataCenterID","default");try{idLength =Integer.parseInt(params.getProperty("idLength","8"));}catch(NumberFormatException e){idLength =8;}}}
使用这个配置,来测试上面的测试,将设置id那行注释掉,查看执行的sql,
INSERT INTO t_idcard (create_time,last_operate_time,version,address,card_no,id)VALUES('2017-07-25','2017-07-25',0,'AD','12','d115009536083520')INSERT INTO userinfo (create_time,last_operate_time,version,age,card_id,phone,username,id)VALUES('2017-07-25','2017-07-25',0,12,'d115009536083520','123','username2','d115009536082770')
我们可以看到自己生成的 插入身份记录的 主键 就是使用我们自定义生成的策略
需要注意的一点,一旦指定了主键生成策略,无论是自定义的,还是系统策略,这时候在自己主动设置主键ID,都是不生效的,也就是在目前这个策略下,就算自己设置 card的主键id 为 ‘2’,也是没作用的。
关联关系和级联级别
@OneToOne
一对一关联关系,通常是一张表有外键,当然也可以两张表都有外键,以用户和身份证两个表为例,
显然用户和身份证是一一对应的关系,来试一下
// 身份证实体类长这样@Data@NoArgsConstructor@EqualsAndHashCode(callSuper =true)@Entity@Table(name ="t_idcard")publicclassIDCardextendsBaseEntity{@Column(name ="card_no", length =20, nullable =false, unique =true)privateString cardNo;@Column(name ="address", length =40)privateString address;}//用户信息实体类长这样@Entity@Table(name ="userinfo", indexes ={@Index(columnList ="phone")}, uniqueConstraints ={@UniqueConstraint(columnNames ={"username"}),@UniqueConstraint(columnNames ={"phone"})})@Data@NoArgsConstructor@EqualsAndHashCode(callSuper =true)publicclassUserInfoextendsBaseEntity{@Column(name ="username", length =40)privateString username;@Column(name ="age")privateInteger age;@Column(name ="phone")privateString phone;@OneToOneprivateIDCard idCard;}
然后运行一下空的测试方法,查看建表语句
CREATE TABLE t_idcard (id VARCHAR (40) NOT NULL,create_time TIMESTAMP NOT NULL,last_operate_time TIMESTAMP NOT NULL,valid boolean DEFAULT TRUE NOT NULL,version INTEGER DEFAULT 1 NOT NULL,address VARCHAR (40),card_no VARCHAR (20) NOT NULL,PRIMARY KEY (id))CREATE TABLE userinfo (id VARCHAR (40) NOT NULL,create_time TIMESTAMP NOT NULL,last_operate_time TIMESTAMP NOT NULL,valid boolean DEFAULT TRUE NOT NULL,version INTEGER DEFAULT 1 NOT NULL,age INTEGER,phone VARCHAR (255),username VARCHAR (40),--关注下面这个字段,这是自动创建的,命名规则为字段名_关联表的主键名id_card_id VARCHAR (40),PRIMARY KEY (id))alter table t_idcard add constraint UK_okqcwtcdgbdm0meqgqhifckk9 unique (card_no)reate index IDXnijxoy2hk6npm7lweieo2w56j on userinfo (phone)alter table userinfo add constraint UK8h620irpir8kcurgsdkhns8lt unique (username)alter table userinfo add constraint UKnijxoy2hk6npm7lweieo2w56j unique (phone)--这里,添加了一个外键alter table userinfo add constraint FK7tk1cso93kbopcvt2mj5yhoru foreign key (id_card_id) references t_idcard
主要注意有 SQL 中有注释的地方,在 @OneToOne 注解后还可以添加 @JoinColumn 注解,来自己明确指定列名,就像下面这样
@OneToOne@JoinColumn(name ="card_id")privateIDCard idCard;
来看下 @OneToOne 注解里面常用的属性,我们先做个简单的测试,在用户表和身份表加一条数据,并且关联
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');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');
然后单元测试一把
/*** 我们在import.sql 里面插入了一条 idcard 和一条用户信息,并且关联了 一对一*/@Testpublicvoid testOrphanRemovalIsFalse(){assertThat(this.userInfoRepository.count()).isEqualTo(1L);assertThat(this.cardRepository.count()).isEqualTo(1L);this.userInfoRepository.delete("1");System.out.println("user count: "+this.userInfoRepository.count());System.out.println("card count: "+this.cardRepository.count());}
注意输出的count,当我们只写了 @OneToOne ,没有配置任何属性,那么user的记录数是0,card的记录数为1,
用户数据的删除,并不影响card表,这在大部分时候都满足需求,但是又是可能需要当用户表的数据删除后,身份信息也删除, 银行只有身份信息对系统来说没有任何左右,
这个时候需求就是当删除用户记录,级联删除身份信息。
查看 @OneToOne 的代码及注释信息,发现有一个属性为 orphanRemoval,注释的大概意思是会级联删除,默认是为 false的,我们设置为true测试看看
@OneToOne(orphanRemoval =true)
SQL 语句
--删除用户DELETEFROMuserinfoWHEREid =?AND version =?| DELETEFROMuserinfoWHEREid ='1'AND version =1--删除身份DELETEFROMt_idcardWHEREid =?AND version =?| DELETEFROMt_idcardWHEREid ='1'AND version =1
测试结果发现确实是级联删除了,
级联删除最好不要配置,因为删除不可控,如果需要删除,最好能够自己主动控制删除。
再看下其他属性,有个 CascadeType 这个也是设置级联级别的,级别有 ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH,
目前我们只对级联删除感兴趣,就是remove,我们配置级联
@OneToOne(cascade ={CascadeType.REMOVE})
经过测试发现,这个确实可以级联删除。
其他属性:
- fetch
获取方式,有懒加载和立即加载,默认是立即,如果设置为懒加载又是可能有出问题,因为正常情况下通过session操作数据库后,session会关闭,这个时候再去访问就会异常了。
如果不是性能特别敏感,或者要加载的字段里面不包含特别大的数据量,还是建议使用立即加载,简单粗暴。 - optional
可选,默认是true,就是这个外键是否允许为null,这个根据需要填写 - mapperBy
这个属性,发现怎么填都是错误的,还是不管这个属性把 - targetEntity
这个会自动帮你处理的,一般情况不需要管他
总的来说,一对一关系还是非常简单的,外键可以在两个表的任意一个表上,或者两个表互相都有外键,
正常情况下使用注解 @OneToOne 和 @JoinColumn 注解就行了,也不需要特别的配置。
上面说了级联删除 CascadeType.REMOTE, 在尝试下级联插入 CascadeType.PERSIST
代码如下
@OneToOne(cascade ={CascadeType.REMOVE,CascadeType.PERSIST})@JoinColumn(name ="card_id")privateIDCard idCard;
测试代码,先各自插入一条记录
@Testpublicvoid testOneToOne01(){//初始插入了一条 身份信息 和 一条用户信息assertThat(this.userRepository.count()).isEqualTo(1L);assertThat(this.cardRepository.count()).isEqualTo(1L);UserInfo userInfo =newUserInfo();userInfo.setUsername("username2");userInfo.setPhone("123");userInfo.setAge(12);userInfo.setId("2");IDCard card =newIDCard();card.setCardNo("12");card.setAddress("AD");card.setId("2");userInfo.setIdCard(card);this.userRepository.save(userInfo);assertThat(this.userRepository.count()).isEqualTo(2L);assertThat(this.cardRepository.count()).isEqualTo(2L);}
可以看到测试是成功的,查下下SQL 的执行情况
INSERT INTO t_idcard (create_time,last_operate_time,version,address,card_no,id)VALUES('2017-07-25','2017-07-25',0,'AD','12','2')INSERT INTO userinfo (create_time,last_operate_time,version,age,card_id,phone,username,id)VALUES('2017-07-25','2017-07-25',0,12,'2','123','username2','2')
先插入了一条 身份信息,然后插入了一条用户信息,所以两个表的记录都是2条,
我们再试一下不配置级联插入,测试一下。
你可以发现出现异常了,因为不会级联插入,就不会先插入身份信息,但是插入用户信息的时候有身份信息的ID,是外键关联,但是身份表没有这个字段,所以包错。
通过上面这个例子就能知道级联插入的作用了。
我们继续测试,配置级联插入,但是不设置 身份的主键ID
@Testpublicvoid testOneToOne01(){//初始插入了一条 身份信息 和 一条用户信息assertThat(this.userRepository.count()).isEqualTo(1L);assertThat(this.cardRepository.count()).isEqualTo(1L);UserInfo userInfo =newUserInfo();userInfo.setUsername("username2");userInfo.setPhone("123");userInfo.setAge(12);userInfo.setId("2");IDCard card =newIDCard();card.setCardNo("12");card.setAddress("AD");// card.setId("2");userInfo.setIdCard(card);this.userRepository.save(userInfo);assertThat(this.userRepository.count()).isEqualTo(2L);assertThat(this.cardRepository.count()).isEqualTo(2L);}
我们会发现,程序还是尝试先级联插入身份记录,但是没有给主键赋值,因此是错误的。
所有要知道级联插入的用法,一旦设置级联插入,当本实体类有其他实体引用,并且要保存的时候,其他实体一定要有他自己的主键,否则会报错。
级联插入还有一个问题,就是当这个引用的实体是从数据库查询出来的,要插入的实体是新建的,这个时候进行插入,还是可能会出现异常。
所以还是尽量不要配置级联删除。
这就会有个问题,一般主键都是业务无关,每次主键都要由应用内设置在保存,显然也不太方便,我们希望主键能按照我们的设想自动生成,例如自定义的全局唯一流水号,之类的。看上面的自定义主键生成策略
在来看下级联更新 CascadeType.MERGE
我们新做一个单元测试
@Testpublicvoid testCascadeMerge(){//初始插入了一条 身份信息 和 一条用户信息assertThat(this.userRepository.count()).isEqualTo(1L);assertThat(this.cardRepository.count()).isEqualTo(1L);UserInfo userInfo =this.userRepository.findOne("1");assertThat(userInfo).isNotNull();IDCard card = userInfo.getIdCard();assertThat(card).isNotNull();userInfo.setAge(44);//修改一下用户的信息,如果不修改用户的信息,就不会触发更新操作card.setAddress("hello world");//修改card的信息this.userRepository.save(userInfo);}
查看这次更新操作执行的SQL
UPDATE userinfoSET create_time ='2017-07-21',last_operate_time ='2017-07-25',version =2,age =44,card_id ='1',phone ='12345678912',username ='luolei'WHEREid ='1'AND version =1
只更新了用户的信息,虽然我们在里面也修改了 身份信息,但是并没有触发更新
现在,配置一下级联更新
@OneToOne(cascade ={CascadeType.REMOVE,CascadeType.PERSIST,CascadeType.MERGE})@JoinColumn(name ="card_id")privateIDCard idCard;
然后再次执行上面的测试,查看 SQL 语句
UPDATE t_idcardSET create_time ='2017-07-21',last_operate_time ='2017-07-25',version =2,address ='hello world',card_no ='123456'WHEREid ='1'AND version =1UPDATE userinfoSET create_time ='2017-07-21',last_operate_time ='2017-07-25',version =2,age =44,card_id ='1',phone ='12345678912',username ='luolei'WHEREid ='1'AND version =1
这次可以看到先触发了身份信息的更新,然后才更新用户信息。
级联更新一般情况下可以配置,通常不会出现什么太大的问题。
级联刷新 CascadeType.REFRESH,
这个就是当获取用户的时候,也会尝试获取最新的身份信息,用的比较少
级联 CascadeType.DETACH
这个不知道是干啥的。。
总结一下,级联默认是没有的,要配置可以配置一下级联更新。
说完级联,再来说下 mapperBy
现在 用户实体长这样
@Entity@Table(name ="userinfo")@Data@NoArgsConstructor@EqualsAndHashCode(callSuper =true)publicclassUserInfoextendsBaseEntity{@Column(name ="username", length =40)privateString username;@Column(name ="age")privateInteger age;@Column(name ="phone")privateString phone;@OneToOne(cascade ={CascadeType.REMOVE,CascadeType.PERSIST,CascadeType.MERGE,CascadeType.DETACH})@JoinColumn(name ="card_id")privateIDCard idCard;}
在里面有身份实体,而且外键在用户表上,列名为 card_id
在身份实体中,之前并没有引用用户实体。如果我想要引用怎么办呢?
一样的,在 IDCard里面加上一个 UserInfo字段,标记 @OneToOne 就行了,
但是这会出现一个问题,就是这是双向关联的,会在身份表上自动生成一个外键,
如果你的需求就是这样,那么在 IDCard 类 UserInfo字段上添加 @JoinColumn 注解自定义一下列名就行了
如果你只是想要一边有外键,只是想在代码中这样使用而已,你需要在 @OneToOne 注解上配置属性 mapperBy
代表维护外键的字段值,这个值是实体类里面的字段名,而不是列名,
例如,现在想外键在userinfo表维护,那么就应该在 IDCard 类那边添加这个 mapperBy ,值为 UserInfo 实体的字段名 idCard
@ManyToOne
多对一关系
这个关系也比较简单,通常是在多的一方有外键。例如 订单 和 订单明细 一个订单 Order 有多个订单明细 OrderItem
OrderItem 就是多的一方,通常是在OrderItem 里面有order的外键关联,正常使用,通常就是加一个 @ManyToOne 注解 加上一个 @JoinColumn 注解
@Data@NoArgsConstructor@EqualsAndHashCode(callSuper =true)@Entity@Table(name ="t_order_item")publicclassOrderItemextendsBaseEntity{@Column(name ="item_id", length =20, nullable =false, unique =true)privateString item_ID;@Column(name ="product_id", length =20, nullable =false)privateString productID;@Column(name ="product_name", length =60)privateString productName;@Column(name ="price", precision =19, scale =2)privateBigDecimal price;@ManyToOne@JoinColumn(name ="order_id")privateOrder order;}
@ManyToOne 里面的属性就没什么好说的了
@JoinColumn 也没啥说的,就是定义关联的字段用的
@OneToMany
还是订单和订单明细的例子,通常,我们拿到订单的时候,都会想要知道订单里面的明细的,订单和明细是一个 一对多 的关系,
在代码层面上就是在Order 类里面有 OrderItem 的集合
@Data@NoArgsConstructor@EqualsAndHashCode(callSuper =true)@Entity@Table(name ="t_order")publicclassOrderextendsBaseEntity{@Column(name ="order_id", length =40, unique =true, nullable =false)privateString orderID;@OneToMany(mappedBy ="order", fetch =FetchType.EAGER)privateList<OrderItem> items;}
需要注意的是,指定 mapperBy 属性,那么就会创建一个中间表,指定的值是Order实体类里面外键的字段名 而不是 列名 ,这个要注意。
还有就是默认的获取方式是 延时加载的,但是在web项目中,可能会出现问题,如果数据量不大,或者性能要求不是非常敏感,可以考虑立即加载
还有就是级联关系了,这个之前分析过了。
@ManyToMany
多对多的关系,这个通常很少。
但是也是有的,譬如角色Role 和 权限 Permission, 这个就是多对多的关系
多对多关系通常都会有一个中间表的,例如 role_permission ,
可以通过 @JoinTable 注解来控制
@Entity@Table(name ="t_role")@Data@NoArgsConstructor@EqualsAndHashCode(callSuper =true)publicclassRoleextendsBaseEntity{@Column(name ="role_name", length =40, nullable =false, unique =true)privateString roleName;@Column(name ="role_desc", length =100)privateString roleDesc;@ManyToMany@JoinTable(name ="t_role_permission",joinColumns ={@JoinColumn(name ="role_id")},inverseJoinColumns ={@JoinColumn(name ="permission_id")},indexes ={@Index(columnList ="role_id")})privateSet<Permission> permissions;}
这个需要说的是,如果没有设置级联关系,新建 Role 里面添加 Permission 的时候,这些Permission 一定是要已经持久化的,否则,Role 保存的时候会出错。
还有当Role里面的Permssion 改变的时候,调用 save方法更新,会自动更新中间表的内容,同样也会自动删除。
对数据库连接信息进行加密
使用依赖
<!-- 加解密配置文件里面 properties --><dependency><groupId>com.github.ulisesbocchio</groupId><artifactId>jasypt-spring-boot-starter</artifactId><version>1.14</version></dependency>
然后在启动类上标记一下,启动这个功能
@SpringBootApplication@EnableJpaAuditing@EnableEncryptablePropertiespublicclassApplication{publicstaticvoid main(String[] args){SpringApplication.run(Application.class, args);}}
添加需要配置的参数
# salt 默认是随机的,随机生成的,等到下次启动应用就变了,我现在想要加密 数据库连接相关信息,肯定不允许改变的jasypt.encryptor.saltGeneratorClassname = org.jasypt.salt.ZeroSaltGenerator# 这个密码就只能明文了jasypt.encryptor.password =Ebnb$2017
单元测试一下,把需要加密的内容先加下密,然后放到配置文件中
@RunWith(SpringRunner.class)@SpringBootTestpublicclassEncryptTest{privatestaticLogger logger =LoggerFactory.getLogger(EncryptTest.class);@AutowiredprivateStringEncryptor encryptor;@Testpublicvoid testEncrypt(){List<String> originStrs =Lists.newArrayList("com.p6spy.engine.spy.P6SpyDriver","jdbc:p6spy:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE","Ebnb$2017");logger.info("--------- 开始加密 ------------");List<String> encryptStrs = originStrs.stream().map(str ->{String encryptStr = encryptor.encrypt(str);logger.info("{}:{}", str, encryptStr);return encryptStr;}).collect(Collectors.toList());logger.info("--------- 结束加密 ------------");logger.info("--------- 开始解密 ------------");encryptStrs.forEach(s -> logger.info("{}:{}", s, encryptor.decrypt(s)));logger.info("--------- 结束解密 ------------");}}
然后将加密过的密文放到配置文件中
#spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriverspring.datasource.driverClassName=ENC(5P1XZXp/AUnwWMSuv/RC5PNQk6Lmy5lSWvbULdWMESzOnCm0LL+SNA==)#spring.datasource.url=jdbc:p6spy:h2:mem:test;DB_CLOSE_ON_EXIT=FALSEspring.datasource.url=ENC(HJpIIcYOhsGYVfMEvSn+6FwKgQKTethY2OJAC1iBbKieTi/BOHkwbMvw2jdu02Cn)
其中 使用 ENC() 包住的才是需要解密的配置。