Java JPA小记

什么是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 --- 以指定字符串结尾

 更多见 官方文档Spring Data JPA


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。示例:

    @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);
View Code

其可以达到目的,但缺点是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,示例:

 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     }
View Code

 

参考资料: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后让框架去自动做转换

@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");
        }
    }
}
View Code

 

 

枚举示例

    @Column(name = "sex")
    @Enumerated(EnumType.ORDINAL)//持久化为0,1
    private Sex sex;

    @Column(name = "type")
    @Enumerated(EnumType.STRING)//持久化为字符串
    private Role role;

 

复杂条件(多条件和多表)查询和分页:Specification

 

 

更多参考资料:

Spring Data JPA 官方文档

使用SPA简化开发-IBM

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值