spring-data-jpa
环境
基础注解
来个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.P6SpyDriver
spring.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:ss
dateformat=
# 打印每条sql 的堆栈
stacktrace=false
# 上面的配置为true,下面的列表中的类的堆栈才打印
stacktraceclass=
# 是否自己加载配置文件
reloadproperties=false
# 加载配置文件间隔,单位秒s,如果上面的配置为true
reloadpropertiesinterval=60
# appender 日志记录的位置
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
#appender=com.p6spy.engine.spy.appender.StdoutLogger
appender=com.p6spy.engine.spy.appender.FileLogger
# 日志名字,只用在FileLogger
logfile = spy.log
# 追加日志
append=false
# 日志信息格式
logMessageFormat=com.p6spy.engine.spy.appender.SingleLineFormat
# 数据库方言日期格式
databaseDialectDateFormat=yyyy-MM-dd
# 配置参数到JMX
jmx=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 resultset
excludecategories=info,debug,result,batch
# 二进制内容是否使用占位符记录
#excludebinary=false
# sql记录门槛 超过时间的sql才会被记录,默认是0,单位毫秒ms 类似慢查询日志
#executionThreshold=
spring-boot
的启动类
@SpringBootApplication
@EnableJpaAuditing
publicclassApplication{
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)
@CreatedDate
privateLocalDateTime createTime;
@Column(name ="last_operate_time", nullable =false)
@LastModifiedDate
privateLocalDateTime 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 项目启动 和关闭 时候执行情况
*/
@Test
publicvoid testNothing(){
}
spring-boot 的启动那些日志我们不去管,我们值关心 JPA
相关的,可以看到在项目目录下生成了一个spy.log 文件,里面是执行的sql语句,把其中的sql语句复制出来,美化一下格式
--1启动的时候,创建表前先删除之前存在的表,这个可以有个配置控制,可以每次启动都创建表然后新建,或者只是更新现有表,或者用以前的表
--我们使用的是内存默认的数据库,因此可以先删除后新建,实际上每次启动都没有表
DROP TABLE userinfo
IF 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 userinfo
IF 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{
// 数据中心ID
privateString dataCenterID;
//主键长度
privateint idLength;
privateAtomicInteger adder =newAtomicInteger(0);
@Override
publicSerializable 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;
}
@Override
publicvoid 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;
@OneToOne
privateIDCard 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 和一条用户信息,并且关联了 一对一
*/
@Test
publicvoid 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 语句
--删除用户
DELETE
FROM
userinfo
WHERE
id =?
AND version =?| DELETE
FROM
userinfo
WHERE
id ='1'
AND version =1
--删除身份
DELETE
FROM
t_idcard
WHERE
id =?
AND version =?| DELETE
FROM
t_idcard
WHERE
id ='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;
测试代码,先各自插入一条记录
@Test
publicvoid 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
@Test
publicvoid 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
我们新做一个单元测试
@Test
publicvoid 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 userinfo
SET create_time ='2017-07-21',
last_operate_time ='2017-07-25',
version =2,
age =44,
card_id ='1',
phone ='12345678912',
username ='luolei'
WHERE
id ='1'
AND version =1
只更新了用户的信息,虽然我们在里面也修改了 身份信息,但是并没有触发更新
现在,配置一下级联更新
@OneToOne(cascade ={CascadeType.REMOVE,CascadeType.PERSIST,CascadeType.MERGE})
@JoinColumn(name ="card_id")
privateIDCard idCard;
然后再次执行上面的测试,查看 SQL 语句
UPDATE t_idcard
SET create_time ='2017-07-21',
last_operate_time ='2017-07-25',
version =2,
address ='hello world',
card_no ='123456'
WHERE
id ='1'
AND version =1
UPDATE userinfo
SET create_time ='2017-07-21',
last_operate_time ='2017-07-25',
version =2,
age =44,
card_id ='1',
phone ='12345678912',
username ='luolei'
WHERE
id ='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
@EnableEncryptableProperties
publicclassApplication{
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)
@SpringBootTest
publicclassEncryptTest{
privatestaticLogger logger =LoggerFactory.getLogger(EncryptTest.class);
@Autowired
privateStringEncryptor encryptor;
@Test
publicvoid 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.P6SpyDriver
spring.datasource.driverClassName=ENC(5P1XZXp/AUnwWMSuv/RC5PNQk6Lmy5lSWvbULdWMESzOnCm0LL+SNA==)
#spring.datasource.url=jdbc:p6spy:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.url=ENC(HJpIIcYOhsGYVfMEvSn+6FwKgQKTethY2OJAC1iBbKieTi/BOHkwbMvw2jdu02Cn)
其中 使用 ENC()
包住的才是需要解密的配置。