好久没写博客了,今天来总结一下JPA的日常使用吧,因为换了工作,以前是用Mybatis Plus,现在要使用JPA。
虽说是总结,也算是学习。
JPA其实按道理来说是一种规范,像Hibernate就是遵循了这种规范。现在最具代表性的就是Spring Data JPA。
闲话不多说,进入正题。
添加依赖
首先就是依赖了,添加依赖
org.springframework.boot spring-boot-starter-data-jpa 然后项目就引入了JPA。使用配置
在application.properties中,添加下面配置
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
#create:每次运行程序时,都会重新创建表,故而数据会丢失
#create-drop:每次运行程序时会先创建表结构,然后待程序结束时清空表
#update:每次运行程序,没有表时会创建表,如果对象发生改变会更新表结构,原有数据不会清空,只会更新(推荐使用)
#validate:运行程序会校验数据与数据库的字段类型是否相同,字段不同会报错
#none: 禁用 DDL 处理
spring.jpa.hibernate.ddl-auto=none
然后就可以使用了,不过Spring Data JPA没有代码生成器,所以从实体类到Controller都要自己来写。这一点就没有Mybatis Plus省事。
常用注解
JPA使用了大量的注解,来进行表—实体类之间的关系映射。来达到简化DAO层,达到少些SQL,甚至不写SQL就可以对数据库进行增删改查的操作。
下面就来介绍一下常用的注解:
@Entity
在类上使用,标明这个类对应数据库中的一个表。
@Table
在类上和@Entity配合使用,来设置对应的表的名字。比如@Table(name = “user”)还有其他可选的属性,如catalog和schema,不过不常用。
@SecondaryTable
在类上使用,一个实体类可以映射多个表,使用这个注解来定义单个从表的名字,主键名字等属性,不经常使用。
@SecondaryTables
这个注解在有多个@SecondaryTable时使用,用来包裹上面的@SecondaryTable。
@Id
在属性上使用,标明数据库表的主键对应的属性。
@GeneratedValue
在属性上使用,在主键属性上,来声明主键的生成策略。如@GeneratedValue(strategy=GenerationType,generator=””),
strategy:表示主键生成策略,有AUTO,INDENTITY,SEQUENCE 和 TABLE
generator:表示主键生成器的名称
Hibernate中有一个@GenericGenerator注解,和这个差不多。
@Column
在属性上使用,来声明该属性对应表中的哪个字段。 如@Column(name = “name”, unique = true, nullable = false, length = 20)
name:数据库表中该字段的名称
nullable:是否允许为null,默认为true
unique:是否是唯一标识,默认为false
length:该字段的大小,仅对String类型的字段有效
还有一些其他的属性设置,不常用就不写了。
@Transient
在属性上使用,表示该属性不是数据库表的字段的映射,即在数据库中没有这个属性对应的字段。
@OneToOne
@JoinColumn
在属性上使用,来声明一对一的表关系。和@JoinColumn一起使用。 如
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(name = “user_id”, referencedColumnName = “id”)
private User user;
targetEntity:表示关联的实体类
fetch:表示抓取策略,默认是懒加载,设置为FetchType.EAGER就是立即加载。
而@JoinColumn来设置数据库关联的字段。
name:列名。
referencedColumnName:该列指向列的列名
简单理解就是name是这个列在自己表中的名字,而referencedColumnName是这个列在关联的其他表的名字。
如果有多个@JoinColumn,可以使用@JoinColumns包裹起来。
@OneToMany
和上面的一样,不过是声明一对多的关系。就是一个自己包含多个其他,用List封装。如
@OneToMany(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(name = “user_id”, referencedColumnName = “id”)
private List users;
@ManyToOne
和@OneToOne差不多。不过是多对一的关系。
@ManyToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(name = “user_id”, referencedColumnName = “id”)
private User user;
@ManyToMany
声明多对多的关系,中间表由ORM框架自动处理。如:
@Entity
public class User {
private List books;
@ManyToMany(targetEntity=package.Book.class)
public List getBooks() {
return books;
}
public void setBooks(List books) {
this.books=books;
}
}
@Entity
public class Book {
private List users;
@ManyToMany(targetEntity=package.Users.class, mappedBy=”books”)
public List getUsers() {
return users;
}
public void setUsers(List users) {
this.users=users;
}
}
targetEntity:表示多对多关联的另一个实体类的全名,例如package.Book.class
mappedBy:表示多对多关联的另一个实体类的对应集合属性名称。但是只有一个实体的@ManyToMany注解需要指定mappedBy属性,指向targetEntity的集合属性名称
@JoinTable
JoinTable在many-to-many关系的所有者一边定义。用于设置中间表。下面就是定义了一个CUST_PHONE中间表,来关联CUST和PHONE。包含两个外键,一个外键是CUST_ID,指向表CUST的主键ID,另一个外键是PHONE_ID,指向表PHONE的主键ID。
@JoinTable(
table=@Table(name=CUST_PHONE),
joinColumns=@JoinColumn(name=“CUST_ID”, referencedColumnName=“ID”),
inverseJoinColumns=@JoinColumn(name=“PHONE_ID”, referencedColumnName=“ID”)
)
@OrderBy
在一对多,多对多关系中,设置从数据库中查询的数据集合对象按照指定方式排序。如
@OneToMany(targetEntity = Book.class, cascade = CascadeType.ALL, mappedBy = “person”)
@OrderBy(name = “isbn ASC, name DESC”)
private List books = new ArrayList();
@IdClass
当使用了复合主键时,需要定义一个类作为id class。 类必须声明为public,并提供一个声明为public的空构造函数。 必须实现Serializable接口,覆写 equals() 和 hashCode()方法。entity class的所有id field在id class都要定义,且类型一样。如
public class EmployeePK implements java.io.Serializable{String empName;Integer empAge;public EmployeePK(){}public boolean equals(Object obj){ ......}public int hashCode(){......}
}@IdClass(value=com.acme.EmployeePK.class)
@Entity(access=FIELD)
public class Employee {@Id String empName;@Id Integer empAge;
}
@MapKey
在一对多,多对多关系中,我们可以用Map来保存集合对象。默认用主键值做key,如果使用复合主键,则用id class的实例做key,如果指定了name属性,就用指定的field的值做key。
@Table(name = "PERSON")
public class Person {@OneToMany(targetEntity = Book.class, cascade = CascadeType.ALL, mappedBy = "person")
@MapKey(name = "isbn")
private Map books = new HashMap();
}
@PrimaryKeyJoinColumn
在@SecondaryTable注解中使用,用于指定从表的主键。
在一对一关系中,用于指定本表主键和另一张表的关联关系。这里类似于@JoinColumn。如
@Entity
@Table(name=“CUSTOMER”)
@SecondaryTable(name=“CUST_DETAIL”,pkJoin=@PrimaryKeyJoinColumn(name=“CUST_ID”,referencedColumnName=“id”))
public class Customer {
@Id(generate = GeneratorType.AUTO)
public Integer getId() {
return id;
}
}
@Table(name = “Employee”)
public class Employee {
@OneToOne
@PrimaryKeyJoinColumn(name = “id”, referencedColumnName=“INFO_ID”)
EmployeeInfo info;
}
@PrimaryKeyJoinColumns
用来包裹@PrimaryKeyJoinColumn。
@Version
Version指定实体类在乐观事务中的version属性
@Lob
Lob指定一个属性作为数据库支持的大对象类型在数据库中存储。
@TableGenerator
TableGenerator定义一个主键值生成器
@SequenceGenerator
SequenceGenerator定义一个主键值生成器
@DiscriminatorColumn
DiscriminatorColumn定义在使用SINGLE_TABLE或JOINED继承策略的表中区别不继承层次的列。
@MappedSuperclass
可以将父类的JPA注解传递给子类,使子类能够继承父类的JPA注解。
@Embeddable
在类上使用,标明该类是可以被嵌入到其他类中的。
@Embedded
在属性上使用,把类嵌入到当前类中,当一个表对应多个实体类,可以用这个注解把类进行组合映射数据库中的表。
@Enumerated
用于标注枚举字段,当不使用任何注解的时候,默认情况下是使用ordinal属性,序号是从0开始。使用该注解之后,数据库中使用枚举类型的name属性。
@Basic
表示一个简单的属性到数据库表的字段的映射。
@Temporal
在进行属性映射时可使用@Temporal注解来调整精度。
@Temporal(TemporalType.TIMESTAMP)// 时间戳
public Date getCreatedTime() {
return createdTime;
}
@Temporal(TemporalType.DATE) //时间精确到天
public Date getBirth() {
return birth;
}
@Query
在Dao接口上使用,用来写SQL或者JPQL。如
//@Query 使用jpql的方式查询。?1代表参数的占位符,其中1对应方法中的参数索引
@Query(value=“from Customer where custName = ?1”)
public Customer findCustomer(String custName);
/**
- nativeQuery : 使用本地sql的方式查询
*/
@Query(value=“select * from cst_customer”,nativeQuery=true)
public void findList();
此外也可以通过使用 @Query 来执行一个更新操作,不过需要在使用 @Query 的同时,用 @Modifying 来将该操作标识为修改查询,这样生成一个更新的操作。
当然JPA提供了大量的查询方法可以使用,很少需要自己写SQL。
Hibernate验证注解
@Pattern
通过正则表达式来验证字符串,如@pattern(regex=”[a-z]{6}”)
@Length
验证字符串的长度,如@Length(min=3,max=20)
@Email
验证一个Email地址是否有效,如@Email
@Range
验证一个整型是否在有效的范围内,如@Range(min=0,max=100)
@Min
验证一个整型必须不小于指定值,如@Min(value=10)
@Max
验证一个整型必须不大于指定值,如@Max(value=20)
@Size
集合或数组的大小是否在指定范围内,如@Size(min=1,max=255)
JPA中的注解真是多啊,说完了注解来看一下其他的高级用法。
方法命名规则查询
除了使用@Query注解,JPA还有一种神奇的查询方式,根据方法名称进行解析,并自动生成查询语句进行查询。 规则如下:
关键字 方法命名 sql where 字句
And findByNameAndPwd where name= ? and pwd =?
Or findByNameOrSex where name= ? or sex=?
Is,Equals findById,findByIdEquals where id= ?
Between findByIdBetween where id between ? and ?
LessThan findByIdLessThan where id < ?
LessThanEquals findByIdLessThanEquals where id <= ?
GreaterThan findByIdGreaterThan where id > ?
GreaterThanEquals findByIdGreaterThanEquals where id > = ?
After findByIdAfter where id > ?
Before findByIdBefore where id < ?
IsNull findByNameIsNull where name is null
isNotNull,NotNull findByNameNotNull where name is not null
Like findByNameLike where name like ?
NotLike findByNameNotLike where name not like ?
StartingWith findByNameStartingWith where name like ‘?%’
EndingWith findByNameEndingWith where name like ‘%?’
Containing findByNameContaining where name like ‘%?%’
OrderBy findByIdOrderByXDesc where id=? order by x desc
Not findByNameNot where name <> ?
In findByIdIn(Collection<?> c) where id in (?) NotIn findByIdNotIn(Collection<?> c) where id not in (?)
True findByAaaTue where aaa = true
False findByAaaFalse where aaa = false
IgnoreCase findByNameIgnoreCase where UPPER(name)=UPPER(?)
好神奇,只要按照方法名称规则命名,不需要写SQL,JPA会自动生成SQL查询,从此不用再写SQL了。
对啦,Dao接口要继承JpaRepository 和 JpaSpecificationExecutor。如
public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExecutor {
}
那个Long是主键的类型。
可以使用JPA增强器zuji-jpa
top.spring-data-jpa zuji-jpa 1.0.1 接口就只需要继承一个BaseRepositoryimport com.ler.demo.domain.User;
import top.springdatajpa.zujijpa.repository.BaseRepository;
public interface UserDao extends BaseRepository {
}
还有一个增强器fenix-spring-boot-starter
Zuji
@GetMapping(“/list”)
public Page list(ReqUserListVO params) {
Specification spec = Specifications.where(e -> {
e.eq(“userType”, params.getUserType())
.contains(“userName”, params.getUserName())
.eq(“assigneeId”, AuthHelper.currentUserId())
.or(e2 -> e2.eq(“status”, “1”).eq(“status”, “2”))
.eq(“deleted”, 0);
});
Sort sort = Sort.by(“createTime”).descending();
return repository.findAll(spec, params.pageRequest(sort));
}
Fenix
List blogs = blogRepository.findAll(builder ->
builder.andIn(“id”, ids, ids != null && ids.length > 0)
.andLike(“title”, params.get(“title”), params.get(“title”) != null)
.andLike(“author”, params.get(“author”))
.andBetween(“createTime”, params.get(“startTime”), params.get(“endTime”))
.build());
分页查询
需要使用JpaSpecificationExecutor接口中的Page findAll(Specification spec, Pageable pageable);这个方法。
root :Root接口,代表查询的根对象,可以通过root获取实体中的属性
criteriaQuery:代表一个顶层查询对象,用来自定义查询
criteriaBuilder:用来构建查询,此对象里有很多条件方法
例子:
if (start == null || start <= 0) {
start = 0;
}
if (size == null || size <= 0) {
size = 10;
}
Specification specification = (Specification) (root, criteriaQuery, criteriaBuilder)
-> criteriaBuilder.like(root.get(“name”).as(String.class), “%” + keyword + “%”);
return userDao.findAll(specification, PageRequest.of(start, size));
上面只有一个查询条件,当有多个条件时,如下:
@GetMapping(value = “/page”)
public Page page(String keyword, Integer age, Integer start, Integer size) {
if (start == null || start <= 0) {
start = 0;
}
if (size == null || size <= 0) {
size = 10;
}
List predicateList = new ArrayList<>();
Specification specification = (Specification) (root, criteriaQuery, criteriaBuilder) -> {
if (StringUtils.isNotBlank(keyword))
predicateList.add(criteriaBuilder.like(root.get(“name”).as(String.class), “%” + keyword + “%”));
if(age != null)
predicateList.add(criteriaBuilder.gt(root.get(“age”).as(Integer.class), age));
return criteriaBuilder.and(predicateList.toArray(new Predicate[0]));
};
return userDao.findAll(specification, PageRequest.of(start, size));
}
这里找了一个完整的例子:
@Override
public List getStudent(String studentNumber, String name, String nickName,
Date birthday, String courseName, float chineseScore, float mathScore,
float englishScore, float performancePoints) {
Specification specification = new Specification() {
@Overridepublic Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {//用于暂时存放查询条件的集合List<Predicate> predicatesList = new ArrayList<>();//--------------------------------------------//查询条件示例//equal示例if (!StringUtils.isEmpty(name)) {predicatesList.add(cb.equal(root.get("name"), name));}//like示例if (!StringUtils.isEmpty(nickName)) {predicatesList.add(cb.like(root.get("nickName"), '%' + nickName + '%'));}//between示例if (birthday != null) {predicatesList.add(cb.between(root.get("birthday"), birthday, new Date()));}//关联表查询示例if (!StringUtils.isEmpty(courseName)) {Join<Student, Teacher> joinTeacher = root.join("teachers", JoinType.LEFT);predicatesList.add(cb.equal(joinTeacher.get("courseName"), courseName));}//复杂条件组合示例if (chineseScore != 0 && mathScore != 0 && englishScore != 0 && performancePoints != 0) {Join<Student, Examination> joinExam = root.join("exams", JoinType.LEFT);Predicate predicateExamChinese = cb.ge(joinExam.get("chineseScore"), chineseScore);Predicate predicateExamMath = cb.ge(joinExam.get("mathScore"), mathScore);Predicate predicateExamEnglish = cb.ge(joinExam.get("englishScore"), englishScore);Predicate predicateExamPerformance = cb.ge(joinExam.get("performancePoints"), performancePoints);//组合Predicate predicateExam = cb.or(predicateExamChinese, predicateExamMath, predicateExamEnglish);Predicate predicateExamAll = cb.and(predicateExamPerformance, predicateExam);predicatesList.add(predicateExamAll);}//--------------------------------------------//排序示例(先根据学号排序,后根据姓名排序)query.orderBy(cb.asc(root.get("studentNumber")), cb.asc(root.get("name")));//--------------------------------------------//最终将查询条件拼好然后returnPredicate[] predicates = new Predicate[predicatesList.size()];return cb.and(predicatesList.toArray(predicates));}
};
return repository.findAll(specification);
总结
JPA功能强大。当用注解关联了表之间关系后,可以自动维护表结构。不过生成环境不建议使用JPA管理表结构。 查询方式灵活,SQL,JPQL,方法命名规则查询,可以根据喜好随意搭配。
当然也有缺点,一个是没有代码生成器,还有就是太灵活了。实体类之间各种嵌入,维护起来比较麻烦。有时候一个表对应四五个实体类,然后实体类里面又嵌入其他的实体类,好几层嵌入,看着真的让人头大。
不过相比于Mybatis-Plus,需要写的SQL真的少了很多。