什么是JPA
JPA之于ORM(持久层框架,如MyBatis、Hibernate等)正如JDBC之于数据库驱动。
JDBC是Java语言定义的一套标准,规范了客户端程序访问关系数据库(如MySQL、Oracle、Postgres、SQLServer等)的应用程序接口,接口的具体实现(即数据库驱动)由各关系数据库自己实现。
随着业务系统的复杂,直接用JDBC访问数据库对开发者来说变得很繁琐,代码难以维护,为解决此问题,ORM(Object Relation Mapping)框架出现了,如MyBatis、Hibernate等,百花齐放。
爱大一统的Java又出手了,Java针对ORM提出了JPA,JPA 本质上是一种 ORM 规范,不是 ORM 框架,只是定制了一些规范,提供了一些编程的 API 接口,具体实现由 ORM 厂商实现。
Spring Data JPA 其实并不依赖于 Spring 框架。
JPA注解
**@Entity**
@Entity 标注用于实体类声明语句之前,指出该Java 类为实体类,将映射到指定的关系数据库表。(类似的,使用@Document可以映射到mongodb)
**@Table**
当实体类与其映射的数据库表名不同名时需要使用 @Table 标注说明,该标注与 @Entity 标注并列使用
- schema属性:指定数据库名
- name属性:指定表名,不知道时表名为类名
**@IdClass**
修饰在实体类上,指定联合主键。如:@IdClass(StudentExperimentEntityPK.class),主键类StudentExperimentEntityPK需要实现Serializable接口
**@id**
@Id 标注用于声明一个实体类的属性映射为数据库的一个主键列
@Id标注也可置于属性的getter方法之前。以下注解也一样可以标注于getter方法前。
若同时指定了下面的@GeneratedValue则存储时会自动生成主键值,否则在存入前用户需要手动为实体赋一个主键值。主键值类型可能是:
-
- Primitive types: boolean, byte, short, char, int, long, float, double.
- Equivalent wrapper classes from package java.lang:
Byte, Short, Character, Integer, Long, Float, Double. - java.math.BigInteger, java.math.BigDecimal.
- java.lang.String.
- java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp.
- Any enum type.
- Reference to an entity object.
- composite of several keys above
**@EmbeddedId**
功能与@IdClass一样用于指定联合主键。不同的在于其是修饰实体内的一个主键类变量,且主键类应该被@Embeddable修饰。
此外在主键类内指定的字段在实体类内可以不再指定,若再指定则需为@Column加上insertable = false, updatable = false属性
**@GeneratedValue**
@GeneratedValue 用于标注主键的生成策略,通过 strategy 属性指定。默认情况下,JPA 自动选择一个最适合底层数据库的主键生成策略:SqlServer 对应 identity,MySQL 对应 auto increment
- IDENTITY:采用数据库 ID自增长的方式来自增主键字段,Oracle 不支持这种方式
- AUTO: JPA自动选择合适的策略,是默认选项
- TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。
- SEQUENCE:通过序列产生主键,通过 @SequenceGenerator 注解指定序列名,MySql 不支持这种方式
**@Basic**
表示一个简单的属性到数据表的字段的映射,对于没有任何标注的 getXxx() 方法,默认为 @Basic
fetch 表示属性的读取策略,有 EAGER 和 LAZY 两种,分别为主支抓取和延迟加载
optional 表示该属性是否允许为 null,默认为 true
**@Column**
当实体的属性与其映射的数据库表的列不同名时需要使用 @Column 标注说明,还有属性 unique、nullable、length 等
**@Transient**
表示该属性并非一个到数据库表的字段的映射,ORM 框架将忽略该属性
如果一个属性并非数据库表的字段映射,就务必将其标识为 @Transient,否则ORM 框架默认为其注解 @Basic,例如工具方法不需要映射
**@Temporal**
在 JavaAPI 中没有定义 Date 类型的精度,而在数据库中表示 Date 类型的数据类型有 Date,Time,TimeStamp 三种精度(日期,时间,两者兼具),进行属性映射的时候可以使用 @Temporal 注解调整精度
SPA使用小记
JPA查询
在查询时,通常需要同时根据多个属性进行查询,且查询的条件也格式各样(大于某个值、在某个范围等等),Spring Data JPA 为此提供了一些表达条件查询的关键字,大致如下:
And --- 等价于 SQL 中的 and 关键字,比如 findByUsernameAndPassword(String user, Striang pwd); Or --- 等价于 SQL 中的 or 关键字,比如 findByUsernameOrAddress(String user, String addr); Between --- 等价于 SQL 中的 between 关键字,比如 findBySalaryBetween(int max, int min); LessThan --- 等价于 SQL 中的 "<",比如 findBySalaryLessThan(int max); GreaterThan --- 等价于 SQL 中的">",比如 findBySalaryGreaterThan(int min); IsNull --- 等价于 SQL 中的 "is null",比如 findByUsernameIsNull(); IsNotNull --- 等价于 SQL 中的 "is not null",比如 findByUsernameIsNotNull(); NotNull --- 与 IsNotNull 等价; Like --- 等价于 SQL 中的 "like",比如 findByUsernameLike(String user); NotLike --- 等价于 SQL 中的 "not like",比如 findByUsernameNotLike(String user); OrderBy --- 等价于 SQL 中的 "order by",比如 findByUsernameOrderBySalaryAsc(String user); Not --- 等价于 SQL 中的 "! =",比如 findByUsernameNot(String user); In --- 等价于 SQL 中的 "in",比如 findByUsernameIn(Collection<String> userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数; NotIn --- 等价于 SQL 中的 "not in",比如 findByUsernameNotIn(Collection<String> userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;
Containing --- 包含指定字符串
StargingWith --- 以指定字符串开头
EndingWith --- 以指定字符串结尾
SPA List类型查询参数: List<StudentEntity> getByIdInAndSchoolId(List<String> studentIdList, String schoolId); ,关键在于 In 关键字。
SPA分页或排序:可以在Repository的方法的最后加一个Sort 或者 Pageable 类型的参数,以便按规则进行排序或者分页查询(编译后会自动在语句后加order by或limit语句)。
Repository中的一个方法myGetByCourseIdAndStudentId:
@Query("select se from StudentExperimentEntity se where se.studentId= ?2 and se.experimentId in ( select e.id from ExperimentEntity e where e.courseId= ?1 ) ")
List<StudentExperimentEntity> myGetByCourseIdAndStudentId(String courseId, String studentId, Pageable pageable);//没有写上述@Query语句也可以加Pageable。虽然实际传值时传PageRequest对象,但若这里生命为PageRequest则不会分页,总是返回所有数据,why?
调用:
studentExperimentRepository.myGetByCourseIdAndStudentId(courseId, studentId, PageRequest.of(0, count, new Sort(Sort.Direction.DESC, "lastopertime")));
编译后会在myGetByCourseIdAndStudentId所写SQL后自动加上 order by studentexp0_.lastopertime desc limit ?
Repository中更新或创建并返回该Entity:如 UserEntity u=userRepository.save(userEntity) ,其中UserEntity包含成员变量private SchoolEntity schoolEntity。Repository的save方法会返回被save的entity,但若是第一次保存该entity(即新建一条记录)时u.schoolEntity的值会为null,解决:用saveAndFlush
返回Entity中的部分字段:
对于nativeQuery,直接select部分字段即可,结果默认会自动包装为Map。为了便于理解可以直接将结果声明为Map。示例:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
@Query(value = "select g.id, g.school_id as schoolId, g.name, g.createtime, g.bz, count(s.id) as stuCount from grade g left join student s " + " on g.name=s.grade where g.school_id=(select a.school_id from admin a where a.id=?1)" // 以下为搜索条件 + " and (?4 is null or g.name like %?4% or g.bz like %?4% ) " + " group by g.id limit ?2,?3", nativeQuery = true) List<Map<String, Object>> myGetGradeList(String adminId, Integer page, Integer size, String searchGradeNameOrGradeBz);
其可以达到目的,但缺点是sql里用的直接是数据库字段名,导致耦合大,数据库字段名一变,所有相关sql都得相应改变。
对于非nativeQuery:(sql里的字段名是entity的字段名,数据库字段名改动只要改变entity中对应属性的column name即可,解决上述耦合大的问题)
当Repository返回类型为XXEntity或List<XXEntity>时通常默认包含所有字段,若要去掉某些字段,可以去掉XXEntity中该字段的get方法。此法本质上还是查出来了只是spring在返回给调用者时去掉了。治标不治本。
也可以自定义一个bean,然后在Repository的sql中new该bean。此很死板,要求new时写bean的全限定名,比较麻烦。
更好的办法是与nativeQuery时类似直接在sql里select部分字段,不过非nativeQuery默认会将结果包装为List而不是Map,故不同的是:这里需要在sql里new map,此'map'非jdk里'Map';需要为字段名取别名,否则返回的Map里key为数值0、1、2... 。示例:
//为'map'不是'Map' @Query("select new map(g.name as name, count(s.id) as stuCount) from GradeEntity g, StudentEntity s where g.name=s.grade and g.schoolId=?1 group by g.id") List<Map<String, Object>> myGetBySchoolId(String schoolId);
Repository nativeQuery返回Entity:
使用nativeQuery时SQL语句查询的字段名若没as则是数据库中的字段名,如school_id,而API返回值通常是schoolId,可以在SQL里通过 school_id as schoolId取别名返回。然而若查询很多个字段值则得一个个通过as取别名,很麻烦,可以直接将返回值指定为数据库表对应的Entity,不过此法要求查询的是所有字段名,如:
@Query(value = " select t.* from teacher t where t.school_id=?1 "// 以下为搜索字段 + "and (?4 is NULL or name like %?4% or job_number like %?4% or bz like %?4% or phone like %?4% or email like %?4%) " + " order by job_number limit ?2, ?3 ", nativeQuery = true) List<TeacherEntity> myGetBySchoolIdOrderByJobNumber(String schoolId, int startIndex, Integer size, String searchNameOrJobnumOrBzOrPhoneOrEmai);// nativeQuery返回类型可以声明为Entity,会自动进行匹配,要求查回与Entitydb中字段对应的所有db中的字段
SPA的update、delete:(需要加@Transactional、@Modefying)
@Transactional //也可以放在service方法上
@Modifying @Query("delete from EngineerServices es where es.engineerId = ?1") int deleteByEgId(String engineerId);
更简单地,可以与query类似,直接:
int deleteByEgId(String engineerId);
SPA的count:
Integer countByName(String name);
级联操作(CASCADE):
Use of the cascade annotation element may be used to propagate the effect of an operation to associated entities. The cascade functionality is most typically used in parent-child relationships.
用于有依赖关系的实体间(@OneToMany、@ManyToOne、@OneToOne等)的级联操作:当对一个实体进行某种操作时,若该实体加了与该操作相关的级联标记,则该操作会传播到该实体关联的实体。包括:
CascadeType.PERSIST:持久化,即保存
CascadeType.REMOVE:删除
CascadeType.MERGE:更新或查询
CascadeType.REFRESH:级联刷新,即在保存前先更新别人的修改:如Order、Item被用户A、B同时读出做修改且B的先保存了,在A保存时会先更新Order、Item的信息再保存。
CascadeType.DETACH:级联脱离,如果你要删除一个实体,但是它有外键无法删除,你就需要这个级联权限了。它会撤销所有相关的外键关联。
CascadeType.ALL:上述所有
注:
级联应该标记在One的一方。如对于 @OneToMany的Person 和 @ManyToOne的Phone,若将CascadeType.REMOVE标记在Phone则删除Phone也会删除Person,显然是错的。
慎用CascadeType.ALL,应该根据业务需求选择所需的级联关系,否则可能酿成大祸。
延迟加载与立即加载(FetchType):通常可以在@OneToMany中用LAZY、在@ManyToOne/Many中用EAGER,但不绝对,看具体需要。
FetchType.LAZY:延迟加载,在查询实体A时,不查询出关联实体B,在调用getxxx方法时,才加载关联实体,但是注意,查询实体A时和getxxx必须在同一个Transaction中,不然会报错:no session
FetchType.EAGER:立即加载,在查询实体A时,也查询出关联的实体B
like查询
对于单字段的可以直接在方法名加Containing
@Query("select s from SchoolEntity s where s.customerId=?1 "// 以下为搜索条件 + " and (?2 is null or s.name like %?2% or s.bz like %?2% ) ") List<SchoolEntity> getByCustomerId(String customerId, String searchSchoolnameOrBz, Pageable pageable);
Entity中将任意对象映射为一个数据库字段:借助JPA converter to map your Entity to the database.
在要被映射的字段上加上注解: @Convert(converter = JpaConverterJson.class)
实现JpaConverterJson:
public class JpaConverterJson implements AttributeConverter<Object, String> {//or specialize the Object as your Column type private final static ObjectMapper objectMapper = new ObjectMapper(); @Override public String convertToDatabaseColumn(Object meta) { try { return objectMapper.writeValueAsString(meta); } catch (JsonProcessingException ex) { return null; // or throw an error } } @Override public Object convertToEntityAttribute(String dbData) { try { return objectMapper.readValue(dbData, Object.class); } catch (IOException ex) { // logger.error("Unexpected IOEx decoding json from database: " + dbData); return null; } } }
需要注意的是,若Entity字段是一个 JavaBean 或 JavaBean 列表(如 TimeSlice 或 List<TimeSlice> ),则反序列化时相应地会反序列化成 LinkedHashMap 或 List<LinkedHashMap>,故强转成TimeSlice或List<TimeSlice>虽然编译期不会报错但运行时就出现类型转换错误。故需要进一步转换成JavaBean,示例:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public static class TimeTableConverter implements AttributeConverter<List<TimeSlice>, String> {// or specialize the Object as your Column type 2 3 private final static ObjectMapper objectMapper = new ObjectMapper(); 4 5 @Override 6 public String convertToDatabaseColumn(List<TimeSlice> data) { 7 try { 8 return objectMapper.writeValueAsString(data); 9 } catch (JsonProcessingException ex) { 10 throw new ApiCustomException(ApiErrorCode.OTHER, "fail to convert list to string"); 11 } 12 } 13 14 @Override 15 public List<TimeSlice> convertToEntityAttribute(String dbData) { 16 // return objectMapper.readValue(dbData, List.class);//直接return会报ClassCastException 17 18 Field[] fields = TimeSlice.class.getDeclaredFields(); 19 20 try { 21 List<Map<String, Object>> tmpMapList = objectMapper.readValue(dbData, List.class);// 对于键值对类型元素,默认反序列化成LinkedListMap类型,故需进一步转换成TimeSlice 22 List<TimeSlice> timeSliceList = null; 23 if (null != tmpMapList) { 24 timeSliceList = new ArrayList<>(); 25 for (Map<String, Object> map : tmpMapList) { 26 TimeSlice tmpTimeSlice = new TimeSlice(); 27 timeSliceList.add(tmpTimeSlice); 28 for (Field field : fields) {// 复制出所有属性 29 try { 30 field.setAccessible(true); 31 field.set(tmpTimeSlice, map.get(field.getName())); 32 } catch (IllegalArgumentException | IllegalAccessException e) { 33 e.printStackTrace(); 34 } 35 } 36 } 37 } 38 return timeSliceList; 39 } catch (IOException ex) { 40 throw new ApiCustomException(ApiErrorCode.OTHER, "fail to convert string to list"); 41 } 42 } 43 }
参考资料:https://stackoverflow.com/questions/25738569/jpa-map-json-column-to-java-object
将任意非基本数据类型(如java bean、list等)对应到数据库字段:
本质上就是将数据序列化成基本数据类型如String。如要把List<String> gradeIdList对应到数据库中的字符串类型的courseSchedule字段。
法1:可以在业务层写代码将gradeIdList序列化成String: String res=objectMapper.writeValueAsString(gradeIdList);// 借助objectMapper.writeValueAsString(data); ,之后保存即可。从数据库中读取时: List<String> gradeIdList=objectMapper.readValue(dbData, List<String>.class); 。此法可以解决问题,但每个字段都得自己手动写此过程。
法2:实现一个AttributeConverter,并应用于Entity字段。此法相当于指定了AttributeConverter后让框架去自动做转换
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
@Column(name = "course_schedule") @Convert(converter = MyJpaConverterJson.class) private List<String> courseSchedule; public class MyJpaConverterJson implements AttributeConverter<List<String>, String> {// or specialize the Object as your // Column type private final static ObjectMapper objectMapper = new ObjectMapper(); @Override public String convertToDatabaseColumn(List<String> data) { try { return objectMapper.writeValueAsString(data); } catch (JsonProcessingException ex) { throw new ApiCustomException(ApiErrorCode.OTHER, "fail to convert list to string"); } } @Override public List<String> convertToEntityAttribute(String dbData) { try { return objectMapper.readValue(dbData, List.class); } catch (IOException ex) { throw new ApiCustomException(ApiErrorCode.OTHER, "fail to convert string to list"); } } }
枚举示例
@Column(name = "sex") @Enumerated(EnumType.ORDINAL)//持久化为0,1 private Sex sex; @Column(name = "type") @Enumerated(EnumType.STRING)//持久化为字符串 private Role role;
复杂条件(多条件和多表)查询和分页:Specification
更多参考资料: