前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈jpa以及增加缓存

浅谈jpa以及增加缓存

原创
作者头像
隋政文
发布2020-07-07 16:56:10
1.8K0
发布2020-07-07 16:56:10
举报
文章被收录于专栏:spring boot 相关

1. jpa介绍

1.1jpa

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的使用方式。

1.2jpa的使用

首先引入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来进行数据库连接池的管理,所以我们也无须关心数据库的连接。

1.3jpa之hibernate的自动更新问题

项目使用中会用到很多配置,所以我们的项目中把配置集中导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的一个特性,在事务型的业务代码里面要注意规避这个问题。

2. jpa增加缓存

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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. jpa介绍
    • 1.1jpa
      • 1.2jpa的使用
        • 1.3jpa之hibernate的自动更新问题
        • 2. jpa增加缓存
        相关产品与服务
        云数据库 SQL Server
        腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档