赞
踩
场景:整合第三方ORM框架,建立一种标准的方式ORM 访问数据库的统一。
现阶段JPA几乎都是接口,实现都是Hibernate在做。我们都知道,在使用持久化工具的时候,一般都有一个对象来操作数据库,在原生的Hibernate中叫做Session,在MyBatis中叫做SqlSession,而在JPA中叫做EntityManager通过这个对象来操作数据库。
对象关系映射(Object Relational Mapping,简称ORM)
通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。
JPA里占位符:?1
(必须按顺序传参)或 :userName
(推荐,本地sql中出现的冒号需要通过双斜杠转义)。
JSR 338主要定义了如何通过普通的Java domain进行关系数据库的操作及映射,概括如下:
@PersistenceContext
private EntityManager em;
通过Persistence Unit创建EntityManagerFactory,再通过EntityManagerFactory获取EntityManager。
EntityManager是JPA中用于增删改查的接口,它的作用是:对一组实体类(Entity Bean)与底层数据源(tabel或临时表)之间进行 O/R 映射的管理。
状态名 | 描述 | 作为java对象存在 | 在实体管理器中存在 | 在数据库存在 |
---|---|---|---|---|
New(瞬时对象) | 尚未有id,还未和Persistence Context建立关联的对象 | yes | no | no |
Managed(持久化受管对象) | 有id值,已经和Persistence Context建立了关联的对象 | yes | yes | yes |
Detached(游离态离线对象) | 有id值,但没有和Persistence Context建立关联的对象 | no | no | no |
Removed(删除的对象) | 有id值,尚且和Persistence Context有关联,但是已经准备好从数据库中删除 | yes | yes | no |
如果entity的主键不为空,而数据库没有该主键,会抛出异常;
如果entity的主键不为空,而数据库有该主键,且entity的其他字段与数据库不同,persist后不会更新数据库;
如果主键格式不正确,会抛出illegalArgumentException异常;
如果主键在数据库未找到数据返回null;
只能将Managed状态的Entity实例删除,由此Entity实例状态变为Removed;
场景举例:
从数据查出对象A
B.list=A.list
修改B.list
save(B)
最后发现往往A在数据库种的值也跟着改变了,为避免这种情况,在每次save之前em.clear() 一下
加载Entity实例后,数据库该条数据被修改,refresh该实例,能得到数据库最新的修改,覆盖原来的Entity实例。
A. 容器托管(EntityManger && PersistenceContext)
@PersistenceContext 也可以用@Autowired注解。
EntityManager em;
@PersistenceContext是jpa专有的注解(推荐),而@Autowired是spring自带的注释。
@AutowiredEntityManager不是线程安全的,当多个请求进来的时候,spring会创建多个线程,而@PersistenceContext就是用来为每个线程创建一个EntityManager的,而@Autowired就只创建了一个,为所有线程共用,有可能报错。
B. 应用托管(EntityManagerFactory && PersistenceUnit)
EntityManagerFactory 接口主要用来创建 EntityManager 实例。该接口约定了如下4个方法:
createEntityManager():用于创建实体管理器对象实例。
createEntityManager(Map map):用于创建实体管理器对象实例的重载方法,Map 参数用于提供 EntityManager 的属性。
isOpen():检查 EntityManagerFactory 是否处于打开状态。实体管理器工厂创建后一直处于打开状态,除非调用close()方法将其关闭。
close():关闭 EntityManagerFactory 。 EntityManagerFactory 关闭后将释放所有资源,isOpen()方法测试将返回 false,其它方法将不能调用,否则将导致IllegalStateException异常。
最顶层的接口,是一个空接口,目的是为了统一所有的Repository的类型,且能让组件扫描时自动识别。
Repository的子接口,提供CRUD 的功能。
CrudRepository的子接口, 添加分页排序。
PagingAndSortingRepository的子接口,增加批量操作等。
delete删除或批量删除
findOne查找单个
findAllByIdAndName查找所有
save保存单个或批量保存
saveAndFlush保存并刷新到数据库
countByInvestUserId
方法 | 说明 |
---|---|
dao.flush () | 同步持久上下文环境,即将持久上下文环境的所有未保存实体的状态信息保存到数据库中。 |
dao.refresh (Object entity) | 用数据库实体记录的值更新实体对象的状态,即更新实例的属性值。 |
dao.clear () | 清除持久上下文环境,断开所有关联的实体。如果这时还有未提交的更新则会被撤消。 |
dao.contains (Object entity) | 判断一个实例是否属于当前持久上下文环境管理的实体。 |
dao.isOpen () | 判断当前的实体管理器是否是打开状态。 |
dao.getTransaction () | 返回资源层的事务对象。EntityTransaction实例可以用于开始和提交多个事务。 |
dao.close () | 关闭实体管理器。之后若调用实体管理器实例的方法或其派生的查询对象的方法都将抛出 IllegalstateException 异常,除了getTransaction 和 isOpen方法(返回 false)。不过,当与实体管理器关联的事务处于活动状态时,调用 close 方法后持久上下文将仍处于被管理状态,直到事务完成。 |
用来做复杂查询的接口。
由于引入了starter-data-jpa,自动配置,spring启动时会实例化一个Repositories,然后扫描包,找出所有继承Repository的接口(除@NoRepositoryBean注解的类),遍历装配。为每一个接口创建相关实例。
SimpleJpaRespositry——用来进行默认的DAO操作,是所有Repository的默认实现
JpaRepositoryFactoryBean——装配bean,装载了动态代理Proxy,会以对应的DAO的beanName为key注册到DefaultListableBeanFactory中,在需要被注入的时候从这个bean中取出对应的动态代理Proxy注入给DAO
JdkDynamicAopProxy——动态代理对应的InvocationHandler,负责拦截DAO接口的所有的方法调用,然后做相应处理,比如findByUsername被调用的时候会先经过这个类的invoke方法
导入jpa,自动配置扫描包内所有继承了Repository接口的接口,为每一个接口实例一个代理类(SimpleJpaRepository),并为每个方法确定一个query策略,已经其他所需的bean,使用自定的repository时,是使用的代理类,经过一些列的拦截器后,选取一个query执行器JpaQueryExecution,然后创建Query对象,拼接sql,组装参数等DB操作,最后返回Query.getResultList()。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
@Table(name=userinfoTab)
@Entity
public class Userinfo implements Serializable {
//映射到数据库表的主键属性
@Id
//主键的生成策略(自增)
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
...
}
属性 | 说明 |
---|---|
name | 默认为非限定类名,用在HQL查询中标识实体 |
属性 | 说明 |
---|---|
name | 表名,缺省以类名做表名 |
catalog | 设置表所属的数据库目录 |
schema | 设置表所属的模式 |
@Id
@idClass
或 @EmbeddedId
@GeneratedValue
@Id
一起使用;strategy=GenerationType.AUTO
strategy策略 | 说明 | 备注 |
---|---|---|
IDENTITY | 自增方式 | Oracle 不支持这种方式; |
AUTO | 默认值;JPA自动选择合适的策略; | 默认情况下,SqlServer 对应 identity,MySQL 对应 auto increment。 |
SEQUENCE | 通过序列产生主键,通过 @SequenceGenerator 注解指定序列名 | MySql 不支持这种方式 |
TABLE | 通过表产生主键,使用该策略可以使应用更易于数据库移植。 |
编写一个复合主键的类CustomerPK,代码如下:CustomerPK.java
作为符合主键类,要满足以下几点要求。
@Data自动生成构造函数: CustomerPK(),CustomerPK(String name, String email);set,get方法;hashCode() ,equals(Object obj) 方法。
equals方法用于判断两个对象是否相同,EntityManger通过find方法来查找Entity时,是根据equals的返回值来判断的。本例中,只有对象的name和email值完全相同时或同一个对象时则返回true,否则返回false。hashCode方法返回当前对象的哈希码,生成的hashCode相同的概率越小越好,算法可以进行优化。
import java.io.Serializable;
//复合主键对象
@Data
public class CustomerPK implements Serializable {
private String email;
private String name;
}
通过@IdClass注释在实体中标注复合主键,实体代码如下:
@Data
@Entity
@Table(name = "customer")
@IdClass(CustomerPK.class)
public class CustomerEO implements java.io.Serializable {
private Integer id;
@Id
private String name;
@Id
private String email;
@Embeddable
@Data
public class DependentId implements Serializable {
private String name;
private int id
}
@Entity
@Data
public class Employee {
@EmbeddedId
private EmployeeId ids;
private String name;
private String email;
}
想让两个类的属性生成一个数据表,在一个类里这样加入另一个类即可:
@Embedded
private C c;
一个类继承另一个类的所有属性,则在父类里这样写:
@SuppressWarnings("serial")
@Entity
@MappedSuperclass //增加这一行
并把父类的所有属性的private改为protected即可
@OneToOne
@ManyToOne
@OneToOne 并通过@JoinColumn指定了表之间的外键关系
@Entity
public class User{
@OneToOne
@JoinColumn(name="address_id")
private Address address;
}
@ManyToOne 并通过@JoinColumn指定了表之间的外键关系
@OneToMany 并通过@JoinColumn指定了表之间的外键关系
@ManyToMany 并通过@JoinTable指定了表之间的外键关系
@Entity
public class Product{
@ManyToMany
//以关联表product_catalog表示关系。
@JoinTable(name="product_catalog",joinColumns = @JoinColumn(name = "product_id"), inverseJoinColumns = @JoinColumn(name="catalog_id"))
private Set<Catalog> catalogs = new HashSet<Catalog>();
}
@OneToMany(cascade = CascadeType.ALL, mappedBy = "a", fetch = FetchType.EAGER)
fetch属性:
值| 说明
–|:–
FetchType.EAGER | 即时加载;默认。
FetchType.LAZY | 懒加载
optional属性
表示该属性是否允许为null, 默认为true
都可以缺省。
注解 | 作用 | 属性 |
---|---|---|
@Basic | 属性到表字段的简单映射 | |
@Column | 描述表字段信息; 注意:与数据库实际不一致不会报错,只会校验po数据是否符合数据规范。 | name :字段名称,默认为字段名驼峰转下划线;unique : 是否唯一; nullable :可否为空;length :长度;insertable 是否出现在insert语句中;updatable 是否出现在update语句中 |
@Enumerated | 映射枚举类型 | |
@Type | 指定类型 | type :Hibernate类型的全限定类名;parameters :类型所需要的参数 |
@Temporal | 映射日期与时间类型; 适用于java.util.Date和java.util.Calendar | value :要映射的内容。DATE对应 java.sql.Date ,时间精确到天 ; TIME对应 java.sql.Time ; TIMESTAMP对应 java.sql.Timestatmp ,时间戳 |
@Transient | ORM框架将忽略该字段,不映射数据库字段。 | |
@Lob | 声明字段为 Clob 或 Blob 类型。 对应pg中的 oid 数据类型。 |
插入、更新时自动填充字段,时传入当前时间,自动填充。
在Application启动类中添加注解 @EnableJpaAuditing
@CreatedBy
、@PrePersist
、@PreUpdate
import org.springframework.data.annotation.CreatedBy; import javax.persistence.*; 字段注解: /** * 创建人 */ @Column @CreatedBy private String createUser; @PrePersist public void prePersist() { createTime = LocalDateTime.now(); createUser = HttpHelper.getCurrentUsername(); } @PreUpdate public void preUpdate() { updateTime = LocalDateTime.now(); updateUser = HttpHelper.getCurrentUsername(); }
/** * 创建时间 */ @CreatedDate private Date createTime; /** * 更新时间 */ @LastModifiedDate private Date updateTime; /** * 创建时间 (Hibernate) */ @CreationTimestamp private Date createTime; /** * 更新时间 (Hibernate) */ @UpdateTimestamp private Date updateTime;; /** * 创建人 */ @CreatedBy private String createBy; /** * 最后修改人 */ @LastModifiedBy private String lastModifiedBy; /** * Spring Data JPA通过AuditorAware<T>接口获取用户信息, * 其中泛型T可以为String保存用户名,也可以为Long/Integer保存用户ID。 * @author EvanWang * */ @Component public class AuditorConfig implements AuditorAware<String> { /** * 返回操作员标志信息 * * @return */ @Override public Optional<String> getCurrentAuditor() { // 这里应根据实际业务情况获取具体信息 return Optional.of(userName); } }
public interface UserinfoRepository extends JpaRepository<Userinfo, Long> {
}
@Autowired
ThreatRepository threatRepository;
threatRepository.saveAll(list);
JpaRepository是Spring Data JPA提供的一个接口,它继承了PagingAndSortingRepository和CrudRepository接口,提供了一组通用的CRUD操作方法,包括增删改查等操作。
方法 | 说明 | 备注 |
---|---|---|
save(S entity) | 保存或更新 | |
findById(ID id) | 按主键查询 | |
existsById(ID id) | 按主键判断对象是否存在 | |
deleteById(ID id) | 按主键删除 | |
findAll() | 查询 | |
count() | 统计 | |
findAll(Pageable pageable) | 分页查询 | |
List findAll(Sort sort) | 排序并查询 | |
flush() | 将持久化上下文中的数据强制同步到数据库 | |
saveAll(Iterable | 批量保存或更新 | 性能低下,建议直接采用jspring jdbc进行批量插入 |
deleteInBatch(Iterable entities) | 批量删除 | |
deleteAllInBatch() | 批量删除 |
可以不写sql,按约定定义方法即可:(其中,第一段…限定返回条数:TopN
、FirstN
、Distinct
;第二段…表示查询的条件,通过And
、Or
关键词组合多个条件,用Not关键词取反)。
方法 | 说明 | 备注 |
---|---|---|
findByXXX | ||
findAllByXXX | ||
findByNameAndPwd | AND | |
findByNameOrSex | OR | |
findByIdBetween | where id between ? and ? | |
findByIdLessThan findByIdBefore | where id < ? | |
findByIdLessThanEqual | where id <= ? | |
findByIdGreaterThan findByIdAfter | where id > ? | |
findByIdGreaterThanEqual | where id > = ? | |
findByNameIsNull | where name is null | |
findByNameNotNull | where name is not null | 或者isNotNull |
findByNameLike | where name like ? | |
findByNameNotLike | ||
findByNameStartingWith | where name like '?%' | |
findByNameEndingWith | where name like '%?' | |
findByNameContaining | where name like '%?%' | |
findByIdOrderByXDesc | Order By | |
findByNameNot | where name <> ? | |
findByIdIn(Collection<?> c) | IN | 注意,in有长度限制 |
findByIdNotIn(Collection<?> c) | ||
findByAaaTue | where aaa = true | |
findByAaaFalse | ||
findByNameIgnoreCase | where UPPER(name)=UPPER(?) | |
count...By... | 统计 |
@Query(value = "select user_id from userinfo",nativeQuery = true)
List<Long> getUserIdList();
@Modifying
@Query(value = "update userinfo set user_name = :name, user_tel = :tel where user_id = :id",nativeQuery = true)
@Transactional
void updateById(@Param("id") Long id, @Param("name") String name, @Param("tel") String tel);
加上@Modifying
,JPA会以更新类语句来执行,而不再是以查询语句执行。
该注解中有两个属性:
a)flushAutomatically:自动刷新,即执行完语句后立即将变化内容从缓存刷新到磁盘。
b)clearAutomatically:自动清除缓存,即执行完语句后自动清除掉已经过期的实体,比如,我们删除了一个实体,但是在还没有执行flush操作时,这个实体还存在于实体管理器EntityManager中,但这个实体已经过期没有任何用处,直到flush操作时才会被删除掉。如果希望在删除该实体时立即将该实体从实体管理器中删除,则可以将该属性设置为true,会在更新完数据库后会主动清理一级缓存。
自动清理之后还会带来一个新的问题,clear 操作清理的缓存中,还包括提交后未 flush 的数据,例如调用 save 而不是 saveAndFlush 就有可能不会立即将修改内容更新到数据库中,在 save 之后 flush 之前调用 @Modifying(clearAutomatically = true) 修饰的方法就有可能导致修改丢失。如果再要解决这个问题,还可以再加上另外一个属性 @Modifying(clearAutomatically = true, flushAutomatically = true),@Modifying 的 flushAutomatically 属性为 true 时,执行 modifying query 之前会先调用 flush 操作,从而避免数据丢失问题。
c)在实际运行中,clear 和 flush 操作都可能需要消耗一定的时间,要根据系统实际情况可以选择使用其中的一个或两个属性,以保证系统的正确性。
User user = userDao.getOneById(1);
userDao.updateName(user.getId, 'abc');
User user2 = userDao.getOneById(1);
System.out.println(user2.getName()); //输出的是旧数据,不是abc
String name = 'abc';
User user = userDao.getOneById(1);
user.setName(name);
userDao.updateName(user.getId, name);
User user2 = userDao.getOneById(1);
System.out.println(user2.getName()); //输出的是新数据,通过set修改了缓存中的内容
默认情况下,repository 接口中的CRUD方法都是被@Transactional注解修饰了的。
字段有值就更新,没值就用原来的:
/** *复杂JPA操作 使用@Query()自定义sql语句 根据业务id UId去更新整个实体 * 删除和更新操作,需要@Modifying和@Transactional注解的支持 * * 更新操作中 如果某个字段为null则不更新,否则更新【注意符号和空格位置】 * * @param huaYangArea 传入实体,分别取实体字段进行set * @return 更新操作返回sql作用条数 */ @Modifying @Transactional @Query("update HuaYangArea hy set " + "hy.areaName = CASE WHEN :#{#huaYangArea.areaName} IS NULL THEN hy.areaName ELSE :#{#huaYangArea.areaName} END ," + "hy.areaPerson = CASE WHEN :#{#huaYangArea.areaPerson} IS NULL THEN hy.areaPerson ELSE :#{#huaYangArea.areaPerson} END ," + "hy.updateDate = CASE WHEN :#{#huaYangArea.updateDate} IS NULL THEN hy.updateDate ELSE :#{#huaYangArea.updateDate} END ," + "hy.updateId = CASE WHEN :#{#huaYangArea.updateId} IS NULL THEN hy.updateId ELSE :#{#huaYangArea.updateId} END " + "where hy.uid = :#{#huaYangArea.uid}") int update(@Param("huaYangArea") HuaYangArea huaYangArea);
注意:
@Entity
@Id
的注释,属性采用驼峰并对应sql是下划线。query.getResultList()
默认返回List<Object[]>
StringBuffer sql = new StringBuffer();
sql.append("select * from user where id = :id");
Map<String, Object> map = new HashMap<>();
map.put("id", id);
Query query = em.createNativeQuery(sql.toString(), User.class);
//通过setParameter注入参数,有效预防sql注入
map.forEach(query::setParameter);
//UserVo必须添加@Entity注解(配置了Java类与数据库映射)。会在数据库自动建表,可以关闭jpa自动建表功能
List<UserVo> users = query.getResultList();
数量查询:
sql = "select count(1) from..."
Query query = em.createNativeQuery(sql.toString());
Object count = query.getSingleResult();
return Long.valueOf(count);
StringBuilder sb = new StringBuilder();
sb.append("update user set name = :name'")
.append(……);
Query query = em.createNativeQuery(sb.toString());
map.forEach(query::setParameter);
query.executeUpdate();
Query query = em.createNativeQuery(sql.toString(), User.class).setParameter(1, "aaa");
query.setFirstResult(page * size);
query.setMaxResults(size);
list = query.getResultList();
import org.springframework.data.domain.Pageable;
@Query(value = "SELECT* FROM stu_tab ",nativeQuery = true)
List<ThreatEntity> getThreatTrend_3h(Pageable pageable);
@RequestMapping(value = "/testPageable", method = RequestMethod.GET)
public Page<User> testPageable(
@RequestParam("page") Integer page, //页码
@RequestParam("size") Integer size, //每页数量
@RequestParam("sortType") String sortType, //排序方式:ASC/DESC
@RequestParam("sortableFields") String sortableFields //排序字段
) {
//判断排序类型及排序字段
Sort sort = "ASC".equals(sortType) ? Sort.by(Sort.Direction.ASC, sortableFields) : Sort.by(Sort.Direction.DESC, sortableFields);
//获取pageable
Pageable pageable = new PageRequest(page-1,size,sort);
return userRepository.findAll(pageable);
}
踩坑:sql语句中有distinct但无order by。加入pageable后报错如下:
SELECT DISTINCT ON expressions must match initial ORDER BY expressions
原因是:
1,在sql中当order by和distinct同时使用时,如果指定了 SELECT DISTINCT,那么 ORDER BY 子句中的项就必须出现在选择列表中。
2,在sql中加入order by语句即可解决该问题。
3,pageable 中 的sort会自动在sql后面拼接order by语句。
JPA不能识别pg的一些类型:inet,jsonb。为了自动转换需要开发者自定义一些类型,称为方言。
import org.hibernate.dialect.PostgreSQL9Dialect;
import java.sql.Types;
public class CustomPostgreSqlDialect extends PostgreSQL9Dialect {
public CustomPostgreSqlDialect() {
super();
this.registerColumnType(Types.JAVA_OBJECT, "jsonb");
}
}
spring: datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/postgres username: postgres password: 123456 hikari: connection-timeout: 20000 maximum-pool-size: 5 jpa: # 指定数据库管理系统,可省略 database: MYSQL hibernate: # 自动建表 ddl-auto: create #命名策略 naming: strategy: org.hibernate.cfg.ImprovedNamingStrategy database-platform: org.hibernate.dialect.MySQL5InnoDBDialect #打印sql show-sql: true database-platform: com.example.jsonb.config.CustomPostgreSqlDialect properties: hibernate.temp.use_jdbc_metadata_defaults: false logging: level: # 打印日志 org.hibernate.type.descriptor.sql.BasicBinder: trace org.springframework.security: - debug - info org.springframework.web: error # 打印sql参数 org.hibernate.SQL: debug org.hibernate.engine.QueryParameters: debug org.hibernate.engine.query.HQLQueryPlan: debug org.hibernate.type.descriptor.sql.BasicBinder: trace
public class JsonbMapType<T> implements UserType {}
public class JsonbListType<T> implements UserType {
public class InetType<T> implements UserType {}
@Entity @TypeDefs({@TypeDef(name = "JsonbMapType", typeClass = JsonbMapType.class), @TypeDef(name = "JsonbListType", typeClass = JsonbListType.class), @TypeDef(name = "InetType", typeClass = InetType.class)}) @Table(name = "test_table") @Data public class TestObjcetPo implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Type(type = "InetType") @Column(name = "ip", columnDefinition = "inet") private String ip; @Column(name = "test_objcet1s") @Type(type = "JsonbListType") private List<TestObject1> testObject1s; @Temporal(TemporalType.TIMESTAMP) @CreationTimestamp #加了这个注解会自动更新创建时间 @Column(name = "create_time") private Date createTime; @Temporal(TemporalType.TIMESTAMP) @UpdateTimestamp #加了这个注解会自动更新更新时间 @Column(name = "update_time") private Date updateTime; }
@Query(value = "SELECT * FROM test_table WHERE id= :id and host(ip) in (:ipList) ", nativeQuery = true)
List<TestObject> getObjectByIdAndIpList(@Param("id") long id, @Param("ipList") List<String> ipList);
@Transactional(rollbackFor = Exception.class) public int[] saveBatch(List<TestObject> list, Date nowTime) { return jdbcTemplate.batchUpdate("insert into test_tabkle ( update_time, soft_infos, ip) " + "values ( ?, to_json(?::json), ?::inet", new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setObject(1, new Timestamp(nowTime.getTime())); ps.setObject(2, JSON.toJSONString(list.get(i).getSoftInfos())); ps.setObject(3, potentialRiskList.get(i).getIp()); } @Override public int getBatchSize() { return potentialRiskList.size(); } }); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。