整合 Spring Data JPA
首先,添加依赖,代码如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
然后,添加配置文件,代码如下:
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useSSL=false
username: root
password: root
##初始化连接数
initialSize: 5
##最小连接数
minIdle: 5
##最大连接数
maxActive: 20
jpa:
hibernate:
ddl-auto: update
show-sql: true
spring.jpa.hibernate.ddl-auto
属性可以取以下值,其含义如下:
- create:每次加载 Hibernate 时都会删除上一次生成的表,然后根据你的实体类再重新生成新表;
create-drop
:每次加载 Hibernate 时根据实体类生成表,但是 sessionFactory 一关闭,表就自动删除;- update:最常用的属性(推荐属性),第一次加载 Hibernate 时根据实体类会自动建立起表的结构(前提是先建立好数据库),以后加载 Hibernate 时根据实体类自动更新表结构。即使表结构改变了,但表中的行仍然存在,不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,要等应用第一次运行起来后才会创建;
- validate:每次加载 Hibernate 时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但会插入新值。
接着,我们添加实体类,代码如下:
@Table(name = "user")
@Entity
public class User {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(name = "user_name",length = 16,nullable = false)
private String name;
@Column(name = "age",nullable = false)
private Integer age;
@Transient
private String address;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
注意,当在实体类中定义的属性不需要映射到对应数据库表中时,只需要在实体类属性字段上添加 @transient
注解即可。
Spring Boot 整合 JPA 访问数据库非常方便,这也是其被广泛用于操作数据库的主要原因。开发过程中,通常我们会直接继承 JpaRepository 接口实现对数据库表的操作。继承该接口,操作数据库时,只需要让接口中定义的方法名称满足一定的规则,JPA 框架便能根据方法名自动解析产生 SQL 语句。示例代码如下:
public interface UserRepository extends JpaRepository<User,Integer> {
User findByName(String name);
}
如上定义的 findByName 方法经过 JPA 框架解析之后,会自动生成一条按照用户名称查询用户的 SQL 语句,并发送到数据库执行查询操作。
编写 userService 接口和实现类:
public interface IUserService {
User queryUserByName(String name);
void addUser(User user);
}
@Service("userService")
public class UserServiceImpl implements IUserService {
@Autowired
private UserRepository userDao;
@Override
public User queryUserByName(String name) {
return userDao.findByName(name);
}
@Override
public void addUser(User user) {
userDao.save(user);
}
}
编写 Controller:
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private IUserService userService;
@RequestMapping("/queryUserByName")
public User queryUserByName(@RequestParam("name") String name) {
return userService.queryUserByName(name);
}
@RequestMapping(value = "/addUser", method = RequestMethod.GET)
public String queryUserByName(@RequestParam("name") String name, @RequestParam("age") Integer age) {
userService.addUser(new User(name, age));
return "Success";
}
}
利用 Postman 向数据库 user 表中插入三条数据:
在 Postman 输入请求地址:
http://localhost:8080/user/queryUserByName?name=huangchaojun
对查询接口进行测试:
看了上面的实例,相信各位已了解了 JPA 功能的强大之处,不过上面只是一个简单的实例,官网提供了 JPA 规范命名方法名到 SQL 语句映射的实例,如下图所示:
没错,看了上面官网给出的 JPA 持久层方法名称到 SQL 语句的映射之后,相信各位又进一步了解了 JPA 的强大之处。不过对于一些特殊功能,如排序、分页等,需要开发者做一些特殊处理。下面分别讲讲如何利用 JPA 实现排序和分页功能。
根据用户年龄的降序排列查询所有用户信息
在用户信息服务层中添加接口:
@Override
public List<User> findAllUser() {
return userDao.findAll(new Sort(Sort.Direction.DESC,"age"));
}
在 Controller 层中添加接口:
@RequestMapping("/queryAllUser")
public List<User> queryAllUser(){
return userService.findAllUser();
}
启动程序,利用 Postman 测试结果如下:
分页查询
为了让读者看清楚分页相关功能,我这边调用新增用户接口向用户表插入100条数据。
在用户持久层添加根据用户年龄降序排列,然后实现分页查询功能。
在用户服务层定义接口并在实现类中实现接口:
List<User> findUserByPageAndPageSize(int currentPage,int pageSize);
/**
* 对用户年龄降序排列并实现分页查询
* @param currentPage
* @param pageSize
* @return
*/
@Override
public List<User> findUserByPageAndPageSize(int currentPage, int pageSize) {
Sort ageSort = new Sort(Sort.Direction.DESC,"age");
Pageable pageable = PageRequest.of(currentPage,pageSize,ageSort);
Page<User> page = userDao.findAll(pageable);
if(page != null){
return page.getContent();
}
return null;
}
JPA 框架除了能够根据方法名称映射为 SQL 语句操作数据库之外,还为开发者提供了直接利用 SQL 语句操作数据库的功能。
利用本地 SQL 语句对用户表数据进行修改、查询。
在用户持久层接口中,添加根据用户 id 修改用户信息,根据用户 id 查询用户信息接口,代码如下:
@Modifying
@Query(value = "update user set age = ?1 where id = ?2",nativeQuery = true)
void updateUserInfoByUserId(Integer userId);
@Query(value = "select * from user where id = ?1",nativeQuery = true)
User queryUserInfoByUserId(Integer userId);
在用户服务层中,新增根据用户 id 修改用户年龄,根据用户 id 查询用户信息接口定义、实现。
接口定义如下:
void updateUserAgeByUserId(Integer age,Integer userId);
User getUserInfoByUserId(Integer userId);
接口实现,代码如下:
@Override
@Transactional
public void updateUserAgeByUserId(Integer age, Integer userId) {
userDao.updateUserInfoByUserId(age,userId);
}
@Override
public User getUserInfoByUserId(Integer userId) {
return userDao.queryUserInfoByUserId(userId);
}
在控制层新增接口:
@RequestMapping("/updateUserAgeById")
public String updateUserAgeById(Integer age, Integer userId) {
userService.updateUserAgeByUserId(age, userId);
return "success";
}
@RequestMapping("/queryUserByUserId")
public User queryUserByUserId(Integer userId) {
return userService.getUserInfoByUserId(userId);
}
启动程序,利用 Postman 分别对修改用户年龄、根据用户 id 查询用户信息接口进行测试:
通过上面的实例,相信各位读者已经注意到,我在 updateUserAgeById 方法上加了注解 @Transactional
,原因是 JPA 在对数据库进行更新操作时默认需要开启事务,假如不开启事务,程序会报如下错误:
javax.persistence.TransactionRequiredException: Executing an update/delete query;
当开发者直接用本地 SQL 语句操作数据库时,需要在 @Query
注解属性中将 nativeQuery 设置为 true,当本地 SQL 语句对数据库表进行写操作时(包括 Update、Delete),还需要在方法上添加 @Modifying
注解。
需要注意的是,如果用本地 SQL 语句或者 JPQL 查询结果集并非 Entity 时,可以用 Object[]
数组代替,如查询用户信息表中年龄小于指定岁数且对应岁数的人数。
在持久层中添加接口:
@Query(value = "select age,count(*) from user where age < ?1 group by age",nativeQuery = true)
List<Object[]> getUserCount(Integer age);
在服务接口层定义接口,服务实现类中实现接口如下:
List<Map<String,Integer>> queryUserCountByAge(int age);
@Override
public List<Map<String,Object>> queryUserCountByAge(int age) {
List<Object[]> list = userDao.getUserCount(age);
if (!CollectionUtils.isEmpty(list)) {
List<Map<String,Object>> mapList = new ArrayList<Map<String,Object>>();
for(Object[] objects : list){
Map<String,Object> map = new HashMap<String,Object>();
map.put("age",objects[0]);
map.put("userCount",objects[1]);
mapList.add(map);
}
return mapList;
}
return null;
}
控制层接口定义如下:
@RequestMapping("/queryUserCountByAge")
public List<Map<String,Object>> updateUserAgeById(Integer age) {
return userService.queryUserCountByAge(age);
}
启动程序,利用 Postman 测试如下:
整合非关系型数据库 Redis
为了演示 Spring Boot 整合 Redis 相关实例,首先在 CentOS 7 服务器上安装 Redis,过程如下。
1. 安装 GCC,执行命令:yum install -y gcc
;
2. 命令执行结束后,输入 gcc -v
命令检查 GCC 是否安装成功,如果执行命令后输出:gcc version 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC)
类似的信息说明 GCC 安装成功;
3. 执行命令:mkdir -p /usr/local/software/
,再创建目录 /usr/local/software
;
4. 执行命令 cd /usr/local/software/
进入 software 目录,然后执行命令 wget http://download.redis.io/releases/redis-4.0.6.tar.gz
下载 Redis 安装包;
5. 执行命令 tar -zxvf redis-4.0.6.tar.gz -C ../
将 Redis 安装包解压到目录 /usr/local/
中;
6. 执行命令 mv redis-4.0.6/ redis
修改目录名称;
7. 执行命令 cd redis
,再执行命令 make MALLOC=libc
编译 Redis 安装包源文件;
8. 执行命令 cd src && make install
安装 Redis;
9. 新建目录 mkdir -p /usr/local/redis/bin /usr/local/redis/etc
;
10. 执行命令 cd /usr/local/redis/src
,打开 src 目录,执行命令将对应的文件复制到上面新建的 bin、etc 目录下:
cp redis-server ../bin/
cp redis-benchmark ../bin/
cp redis-check-rdb ../bin/
cp redis-sentinel ../bin/
cp redis-cli ../bin/
cp ../redis.conf ../etc/
11. 执行命令 vim /etc/profile
,修改环境变量文件,在文件中添加下面内容:
export REDIS_HOME=/usr/local/redis/
export PATH=${JAVA_HOME}/bin:${REDIS_HOME}/bin:$PATH
保存修改的内容后,执行 source /etc/profile
命令让修改环境变量文件生效。
12. 修改 /usr/local/redis/etc/redis.conf
配置文件,修改内容如下:
bind 192.168.1.120
bind 127.0.0.1
daemonize yes
dir /usr/local/redis/data/
appendonly yes
appendfsync always
以上配置含义以及 Redis 相关配置调优将在后面的 Redis 相关课程中进行详解。
13. 执行命令 redis-server /usr/local/redis/etc/redis.conf
启动 Redis 服务端;
14. 执行命令 ps -ef | grep 6379
检查 redis-server
是否已经正常启动。如输出如下信息说明启动成功:
接下来演示 Spring Boot 如何整合 Redis。
1. 在 pom 文件中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
2. 在配置文件中添加如下内容:
spring:
redis:
#设置数据库索引
database: 0
#Redis服务器地址
host: 192.168.1.120
#Redis服务器连接端口
port: 6379
#Redis服务器连接密码(默认为空)
password: hcb13579
#连接池最大连接数(使用负值表示没有限制)
pool:
max-active: 10
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
#连接池中的最大空闲连接
max-idle: 10
#连接池中的最小空闲连接
min-idle: 0
#连接超时时间(毫秒)
timeout: 60000
3. 添加操作 Redis 服务层接口:
/**
* 利用Redis持久化用户信息
*/
public interface RedisUserService {
/**
* 根据用户uuid获取用户信息
* @param uuid
* @return
*/
User getUserInfo(String uuid);
/**
* 将用户信息存入Redis
* @param user
*/
StringsaveUserInfo(User user);
}
4. 添加操作 Redis 服务实现类:
@Service("redisUserService")
public class RedisUserServiceImpl implements RedisUserService {
@Autowired
private RedisTemplate redisTemplate;
@Override
public User getUserInfo(String uuid) {
return (User) redisTemplate.opsForValue().get(uuid);
}
@Override
public String saveUserInfo(User user) {
redisTemplate.opsForValue().set(user.getUuid(),user);
return user.getUuid();
}
}
5. 在控制层中添加将用户信息存入 Redis、从 Redis 中查询用户信息的接口:
@RequestMapping("/saveUserInfoIntoRedis")
public String saveUserInfoIntoRedis(String name,Integer age){
User user = new User(name,age);
user.setUuid(UUID.randomUUID().toString());
return redisUserService.saveUserInfo(user);
}
@RequestMapping("/getUserInfoFromRedis")
public User saveUserInfoIntoRedis(String uuid){
String realKey = (String) redisTemplate.opsForValue().get(uuid);
return redisUserService.getUserInfo(realKey);
}
6. 启动程序,利用 Postman 测试如下:
实际工作中,Redis 除了用于实现缓存功能之外,其发布、订阅功能也比较重要。
下面为各位读者讲解如何使用这些功能。
首先和大家说一个 Redis 订阅、发布功能在实际工作中的应用场景。在未用 Redis 发布、订阅功能之前,我同事发送一个消息到消息中间件 RabbitMQ 中的某一个队列中,消息包括了能够代表本次请求的唯一 UUID,利用服务器端上另一个运行 Python 脚本的容器监听该队列,然后消费消息,接着将 Python 脚本处理后的结果放在 Redis 中。
Redis 中的 key 用来存放消息中间件的 UUID,value 存放 Python 处理的结果,而在微服务端循环地利用 UUID 去 Redis 中查询 Python 处理的结果。该种处理方式会在发送消息端一直循环,并同步查询结果,在我们不知道 Python 端处理请求需要多长时间时,该种处理方式就显得不太恰当了。
当类似的请求和用户之间有交互时,更是会严重影响用户体验。经过我们代码走查,最后建议将其改为 Redis 订阅、发布功能。这样处理的话,当服务调用方向消息中间件发送消息之后,会及时返回,等 Python 端处理完之后将结果放入 Redis 中,异步处理即可。
下面就为各位演示下 Redis 发布订阅功能如何实现。
1. 定义监听器:
public class RedisChannelListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] bytes) {
byte[] channelBytes = message.getChannel();
byte[] bs = message.getBody();
try {
String content = new String(bs,"UTF-8");
String channel = new String(channelBytes,"UTF-8");
System.out.println("channel:" + channel + "---" + "message:" + content);
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
}
2. 设置监听器:
@Configuration
public class MessageListenerConfig {
@Bean
public MessageListenerAdapter listenerAdapter() {
return new MessageListenerAdapter(new RedisChannelListener());
}
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory redisConnectionFactory){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener(listenerAdapter(),new PatternTopic("redis.news.*"));
return container;
}
}
3. 添加控制器:
@RequestMapping("/redis")
@RestController
public class RedisController {
@Autowired
@Qualifier("publishRedisMessage")
private PublishRedisMessage publishRedisMessage;
@RequestMapping("/publishMessage")
public String publishRedisMessage(String channel,String msg){
return publishRedisMessage.publishMessage(channel,msg);
}
}
4. 启动程序利用 Postman 进行测试即可。