jpa即java persistence api,一个封装比较轻量级的orm框架,底层用了hibernate来实现。jpa诞生的缘由是为了整合第三方ORM框架,建立一种标准的方式,在ORM框架中,Hibernate是一支很大的部队,使用很广泛,也很方便,能力也很强,同时Hibernate也是和JPA整合的比较良好,我们可以认为JPA是标准,事实上也是,JPA几乎都是接口,实现都是Hibernate在做,宏观上面看,在JPA的统一之下Hibernate很良好的运行。
我们都知道,在使用持久化工具的时候,一般都有一个对象来操作数据库,在原生的Hibernate中叫做Session,在JPA中叫做EntityManager,通过这个对象来操作数据库。一般按照mvc分层的架构,那么jpa就是负责DAO层的相关处理,在DAO层面上我们希望看到的都是一个个对象或者个对象的集合,而底层的与数据库相关的操作DAO层我们希望是透明的。像jpa这种ORM的框架本身可以提供什么功能呢?简单来说就是基本的CURD操作,所有基础的CURD操作它均可以提供,如果我们使用原生的框架,那么就要自己实现数据库连接相关,底层sql语句也要自己来实现与维护,这样成本会相当高。jpa的出现,使得jdbc这种关系型数据库的使用变得相当简单,我们基本不需要写sql语句,至少我目前所负责的项目的jpa使用暂还没有需要手写sql的地方。
下面我们来看看jpa的使用方式。
首先引入spring-data-jpa依赖,目前的项目是使用spring boot加gradle来完成构建,下面先直接看下demo。
@Entity
@Table(name = "user")
data class User(
@Id
@javax.persistence.Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Field("id")
@Column(name = "id")
var id: Long = 0,
@Field("username")
var username: String? = null,
@Field("phone")
var phone: String? = null,
@Field("email")
var email: String? = null,
@Field("email")
var age: Long? = null,
)
@Transactional
interface JpaUserRepository : PagingAndSortingRepository<User, Long>, QueryByExampleExecutor<User> {
fun findByUsername(username: String): User?
fun findByPhone(phone: String): User?
fun findByEmail(email: String): User?
}
data class Reuqest(
var username: String? = null,
var phone: String? = null,
var email: String? = null
)
@RestController
@RequestMapping("/user/demo")
class DemoController {
@Autowired
lateinit var jpaUserRepository: JpaUserRepository
@PostMapping("/read")
fun readUserbyUsername(@RequestBody request: Request): User {
return jpaUserRepository.findByUsername(request.username)
}
@PostMapping("/update")
fun updateUserByUsername(@RequestBody request: Request): User {
val user = jpaUserRepository.findByUsername(request.username)
user.phone = request.phone
user.email = request.email
return jpaUserRepository.save(user)
}
@PostMapping("/add")
fun addUser(@RequestBody request: Request): User {
val user = User()
user.phone = request.phone
user.email = request.email
user.username = request.username
return jpaUserRepository.save(user)
}
@PostMapping("/delete")
fun deleteUserByUsername(@RequestBody request: Request): User {
val user = jpaUserRepository.findByUsername(request.username)
return jpaUserRepository.delete(user)
}
}
如上,我们定义了一个对象结构体User,里面有username,email,phone属性,然后使用spring-data-jpa定义了接口JpaUserRepository,然后在repository中定义了业务需要的查询方式,基本查询都是基于findBy开头的,后面的name字段jpa就会将它们翻译成where的查询字段,所以这里我们只需要定义好函数即可,同样也是可以进行批量查询与模糊查询等等操作的, Jpa会让你更加爱上spring boot,很少的代码即可完成基本的CURD业务接口。如下列出一部分操作语句。
Keyword | Sample | JPQL snippet |
---|---|---|
And | findByUsernameAndEmail | where x.username = ?1 and x.email = ?2 |
Or | findByUsernameOrEmail | where x.username = ?1 or x.email = ?2 |
Between | findByAgeBetween | where x.age between ?1 and ?2 |
LessThan | findByAgeLessThan | where x.age < ?1 |
GreaterThan | findByAgeGreaterThan | where x.age > ?1 |
IsNull | findByAgeIsNull | where x.age is null |
isNotNull | findByAgeIsNotNull | where x.age not null |
Like | findByUsernameLike | where x.username like ?1 |
NotLike | findByUsernameNotLike | where x.username not like ?1 |
OrderBy | findByAgeOrderByUsername | where x.age =?1 order by x.username desc |
Not | findByUsernameNot | where x.username <> ?1 |
In | findByAgeIn(Collection<Age> ages) | where x.age in ?1 |
NotIn | findByAgeNotIn(Collection<Age> ages) | where x.age not in ?1 |
如上,我们在进行repository操作时可以使用任意字段组合查询方式,jpa都将翻译成sql,然后由底层的hibernate的session来进行数据层的操作,数据库的连接spring boot采用了开源的HikariCP来进行数据库连接池的管理,所以我们也无须关心数据库的连接。
项目使用中会用到很多配置,所以我们的项目中把配置集中导Config结构体中,且提供了动态配置的使用,即将Config落到DB,所以也由了ConfigRepository。由于开始使用的业务并不多,后续逐步开始接入业务,我们的配置中有一个第三方oauth的复杂配置,可以支持微信,QQ等第三方帐号来登录,在我们配置开放了读写接口的时候遇到一个诡异的问题,发现注册的第三方配置有的时候会丢掉,即当时写请求是成功了,但过了不到一个小时竟然消失了?
下面打开了show-sql:true配置,日志中明显的看到有一个地方进行了update操作。查了相关代码,并没有查到哪里会去写操作。下面看一个demo:
interface DaoService {
fun readUserByUsername(username: String): User
}
@Service
class UserDaoService : DaoService {
@Autowired
lateinit var jpaUserRepository: JpaUserRepository
@Transactional
override fun readUserByUsername(username: String): User {
val user = jpaUserRepository.findByUsername(username)
user.phone = ""
user.email = ""
}
}
@RestController
@RequestMapping("/user/auto_update")
class DemoController {
@Autowired
lateinit var userDaoService: DaoService
@PostMapping("/read")
fun readUserbyUsername(@RequestBody request: Request): User {
return userDaoService.readUserByUsername(request.username)
}
}
问题:首先在db写入一个用户,然后调用/user/auto_update/read读取用户信息,发现调用后,phone,email自动被更新了。我们的动态配置遇到的就是这个问题,这个其实是hibernate的一个特性,当操作的函数声明了是事务类型,那么在repository都操作后不要再进行对象属性的赋值操作,否则事务再走完它自己的session后面会将对象的改变重新写入到db,就会发生没有主动update的时候却自动发生了update的操作。注意这是hibernate的一个特性,在事务型的业务代码里面要注意规避这个问题。
Spring boot支持缓存注解,支持本地缓存,也可以支持数据库缓存,当业务需求,如果分布式访问的话那么就要考虑内存数据库缓存了,一般可以用redis来实现。再次我们项目中采用了redis缓存来提升服务整体的性能。下面介绍以下我是如何在jpa之上增加了redis缓存。
首先我们先来认识几个注解:
1)@EnableCaching
开启缓存功能,一般放在启动类上,也可以放到cacheManager的配置类上,同时可以增加ConditionalOnBean来控制配置类的加载,从而控制缓存的开关。
2)@CacheConfig
当我们需要缓存的地方越来越多,你可以使用@CacheConfig(cacheNames = {"cacheName"})注解在 class 之上来统一指定value的值,这时可省略value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。
3)@Cacheable
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上,它有如下几个属性:
属性 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
chacheNames | 与value差不多,二选一即可 |
key | 可选属性,可以使用SpEL标签自定义缓存的key |
keyGenerator | key的生成器。key/keyGenerator二选一使用 |
cacheManager | 指定缓存管理器 |
chacheResolver | 指定获取解析器 |
condition | 符合条件则进行缓存 |
unless | 符合条件则不进行缓存 |
sync | 是否使用异步模式,默认为false |
4)@CachePut
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上,属性同Cacheable。
5)CacheEvict
使用该注解标志的方法,可以清空指定的缓存,即指定value + key。一般用在更新或删除方法上。属性同Cacheable。
6)Caching
该注解可以实现同一个方法上同时使用多种注解,一般evict的时候会用到这个注解,可以要给方法evict多个缓存。
下面依然用最上面的demo的User来实现redis缓存。代码如下:
@Configuration
@EnableCaching
class RedisConfig : CachingConfigurerSupport() {
@Autowired
lateinit var cloudConfig: ConfigService
@Bean
fun wiselyKeyGenerator(): KeyGenerator {
return KeyGenerator { target, method, params ->
val sb = StringBuilder()
sb.append(target.javaClass.name)
sb.append(sp)
sb.append(method.name)
for (obj in params) {
if (obj == null) continue
sb.append(sp)
sb.append(obj.toString())
}
sb.toString()
}
}
@Bean
fun cacheManager(factory: RedisConnectionFactory): CacheManager {
val valuePair = RedisSerializationContext.SerializationPair.fromSerializer(JdkSerializationRedisSerializer())
val keyPair = RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer())
val defaultCacheConfig = RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith(keyPair)
.serializeValuesWith(valuePair)
.entryTtl(Duration.ofHours(1))
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(factory))
.cacheDefaults(defaultCacheConfig).build()
}
companion object {
const val sp = ':'
}
}
@Transactional
interface JpaUserRepository : PagingAndSortingRepository<User, Long>, QueryByExampleExecutor<User> {
@Cacheable(
value = ["findByUsername"],
key = "'username:' + #username,
unless = "#result == null"
)
fun findByUsername(username: String): User?
@Cacheable(
value = ["findByPhone"],
key = "'phone:' + #phone,
unless = "#result == null"
)
fun findByPhone(phone: String): User?
@Cacheable(
value = ["findByEmail"],
key = "'email:' + #email,
unless = "#result == null"
)
fun findByEmail(email: String): User?
@JvmDefault
@Caching(
evict = [
CacheEvict(
value = ["findByUsername"],
key = "'username:' + #user.username
),
CacheEvict(
value = ["findByPhone"],
key = "'phone:' + #user.phone
),
CacheEvict(
value = ["findByEmail"],
key = "'email:' + #user.email
)
]
)
fun evictOne(user: User){}
}
@RestController
@RequestMapping("/user/demo")
class DemoController {
@Autowired
lateinit var jpaUserRepository: JpaUserRepository
@PostMapping("/read")
fun readUserbyUsername(@RequestBody request: Request): User {
return jpaUserRepository.findByUsername(request.username)
}
@PostMapping("/update")
fun updateUserByUsername(@RequestBody request: Request): User {
val user = jpaUserRepository.findByUsername(request.username)
jpaUserRepository.evictOne(user)
user.phone = request.phone
user.email = request.email
jpaUserRepository.save(user)
return user
}
@PostMapping("/add")
fun addUser(@RequestBody request: Request): User {
val user = User()
user.phone = request.phone
user.email = request.email
user.username = request.username
return jpaUserRepository.save(user)
}
@PostMapping("/delete")
fun deleteUserByUsername(@RequestBody request: Request): User {
val user = jpaUserRepository.findByUsername(request.username)
jpaUserRepository.evictOne(user)
jpaUserRepository.delete(user)
return user
}
}
首先配置redis的cacheManager,并可选择的实现keyGenerator方式。然后直接在repository的接口方法上增加@Cacheable进行缓存处理即可,为了便于控制缓存开关,这里cacheManager可以用ConditionalOnBean开控制是否加载,然后evict的地方和实际的写操作分离,使用配置控制是否调用evict方法,整体可以通过配置来控制缓存的开关。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。