应一个好友要求写一个SpringJDBC 的文章 , 介绍返回操作数据库最新id写法

本文介绍了SpringJdbc的基本使用方法,包括添加、修改、删除及查询等核心操作,并展示了如何利用SpringJdbc进行批量处理和获取自动生成的主键。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring Jdbc 解决了繁琐的statement 创建 连接的打开 关闭 异常的处理

spring给我们提供了jdbc的统一封装,和其它的orm不同,它是对jdbc api的封装,相当于我们自己写的java bean,而没有对对象模型进行封装(比如将po封装为pojo)。它保持了jdbc数据对象的原始性,具有极大程度的灵活性,其性能也极高。


下面所有代码都已经亲测


import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

public class SJTest {

	JdbcTemplate jdbcTemplate = new JdbcTemplate();

	@Before
	public void init() {
		DriverManagerDataSource ds = new DriverManagerDataSource();
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		ds.setUrl("jdbc:mysql://127.0.0.1:3306/test");
		ds.setUsername("root");
		ds.setPassword(""); // 我的数据库没有密码

		jdbcTemplate.setDataSource(ds);
	}
	

	/**
	 * 添加
	 */
	@Test
	public void add() {
		String sql = "insert into student(sid,name) values(?,?)";
		jdbcTemplate.update(sql, new Object[] { 3, "321" });
	}

	/**
	 * 修改
	 */
	@Test
	public void update() {
		String sql = "update student set name=? where sid=?";
		jdbcTemplate.update(sql, new Object[] { "432", 3 });
	}

	/**
	 * 删除
	 */
	@Test
	public void delete() {
		String sql = "delete from student where sid = ?";
		jdbcTemplate.update(sql, new Object[] { 3 });
	}

	/**
	 * 查询在程序设计当中占用的比重较重, 所以我出多种写法 来进行表现
	 * 在spring 中拥有较多查询方法
	 */
	
	
	// ----------------------------------首先介绍 query 开头方法
	
	/**
	 * 在下面的两个方法中 第一个不会产生非常多的系统开销,第二个非常浪费系统性能 不推荐使用 会产生 内存溢出
	 * 产生的原因主要在 用法上 第一种 processRow() 方法里面在内部实现中逐行获取 user 数据
	 * 					  第二种  mapRow() 返回一个集合通过遍历来达成目的
	 * 如果你无法理解 可以在网上查下 RowCallbackHandler 和 RowMapper 的比较
	 */
	
	@Test
	public void select() {
		// org.springframework.jdbc.core.RowCallbackHandler  是一个回调接口
		String sql = "select * from student where sid=?";
		
		final Student student = new Student();
		
		jdbcTemplate.query(sql, new Object[]{8} , new RowCallbackHandler() {
			
			@Override
			public void processRow(ResultSet rs) throws SQLException {
				// TODO Auto-generated method stub
				student.setAge(rs.getString("age"));
				student.setId(rs.getLong("sid"));
			}
		});
		
		System.out.println(student.toString());
	}
	
	@Test
	public void query() {
		String sql = "select * from student";
		
		List<List<Student>> student2 = jdbcTemplate.query(sql, new RowMapper<List<Student>>(){
			
			final List<Student >  student = new ArrayList<Student> ();
			
			@Override
			public List<Student> mapRow(ResultSet rs, int rowNum) throws SQLException {
				final Student stu = new Student();
				stu.setAge(rs.getString("age"));
				stu.setId(rs.getLong("sid"));
				student.add(stu);
				return student;
			}
		});
		System.out.println(Arrays.toString(student2.toArray()));
	}
	
	
	
	/**
	 * 在有些时候我们执行完操作需要返回数据库表的最新id spring 已经帮助我们做好了 不需要再编写sql 语句查询了
	 * 我扩展下 其实spring 也是在jdbc 基础上封装的 很多刚学习的认为spring 怎么怎么样
	 * 在jdbc 中 也有相应地方法我给大家演示下两种 Statement 和 PreparedStatement
	 */
	
	/**
	 * 使用jdbc Statement 返回里面的id 
	 * @throws SQLException 
	 */
	@Test
	public void jdbcNativeID () throws SQLException {
		 String sql = "insert into student(sid,name) values(7,'457')";
		 Connection conn = jdbcTemplate.getDataSource().getConnection();
		 Statement stmt = conn.createStatement();
		 
		 // Statement.RETURN_GENERATED_KEYS 可以绑定数据库产生的id
		 // Statement.NO_GENERATED_KEYS 不产生id 默认可能。
		 stmt.executeUpdate(sql,Statement.RETURN_GENERATED_KEYS);
		 
		 ResultSet rs = stmt.getGeneratedKeys();
		 
		 if(rs.next())
		 System.out.println("这是一个返回 数据库id 的打印 " + rs.getInt(1));
	}
	
	/**
	 * 使用jdbc PreparedStatement 返回里面的id 
	 * @throws SQLException
	 */
	@Test
	public void jdbcNativeId() throws SQLException {
		 String sql = "insert into student(sid,name) values(8,'457')";
		 Connection conn = jdbcTemplate.getDataSource().getConnection();
		 PreparedStatement ps = conn.prepareStatement(sql,PreparedStatement.RETURN_GENERATED_KEYS);
		 ps.executeUpdate();
		 ResultSet rs  = ps.getGeneratedKeys();
		 
		 if(rs.next())
	     System.out.println("这是一个返回 数据库id 的打印 " + rs.getInt(1));
	}
	
	/**
	 * 这是在spring 中返回最新id 的处理方式
	 */
	@Test
	public void resultID() {
		
		KeyHolder keyHolder = new GeneratedKeyHolder();

		final String sql = "insert into student(sid,name) values(?,?)";
		
		jdbcTemplate.update(new PreparedStatementCreator() {

			@Override
			public PreparedStatement createPreparedStatement(Connection con)
					throws SQLException {
				// TODO Auto-generated method stub
				
				PreparedStatement ps = con.prepareStatement(sql);
				ps.setString(1, "4");
				ps.setString(2, "457");
				return ps;
			}
		}, keyHolder);
		
		/**
		 * 可以调用 keyHolder.getKeyList(); 返回 List<Map<String, Object>> 或者
		 * keyHolder.getKeys(); 返回 Map<String, Object>
		 */	
		System.out.println(keyHolder.getKey().intValue());
	}

	
	
	/**
	 * 这是一种spring 批处理方式
	 */
	@Test
	public void batchUpdate() {
		String sql = "insert into student(sid,age) values(?,?)";
		
		final List<Student>  list = new ArrayList<Student>();
		
		for(int i =  1 ;  i< 10; i ++) {
			Student student = new Student();
			student.setAge(i + "1");
			student.setId(9 + i);
			list.add(student);
		}
		
		jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
			
			@Override
			public void setValues(PreparedStatement ps, int i) throws SQLException {
				// TODO Auto-generated method stub
				Student  obj = list.get(i);
				ps.setLong(1, obj.getId());
				ps.setString(2, obj.getAge());
			}
			
			@Override
			public int getBatchSize() {
				// TODO Auto-generated method stub
				return list.size();
			}
		});
	}
	
	class Student {
		private long id;
		private String age;

		public String getAge() {
			return age;
		}

		public void setAge(String age) {
			this.age = age;
		}

		public long getId() {
			return id;
		}

		public void setId(long id) {
			this.id = id;
		}

		@Override
		public String toString() {
			return "Student [id=" + id + ", age=" + age + "]";
		}

		
	}

}


<think>我们已经在之前的回答中提供了一个完整的示例,但根据用户的最新要求,我们需要参考站内引用中的一些做法。结合引用[1]和[3],我们可以调整实现方式: 关键点: 1. 引用[1]展示了在项目启动时将数据加载到Redis,但我们这里需要的是按需加载(根据接口传入的id) 2. 引用[2]提到了使用BitMap和List来防止缓存穿透,我们可以借鉴思路:当查询一个不存在的数据时,避免频繁查询数据库,可以将空值也缓存起来 3. 引用[3]指出,如果缓存了空值,后续即使数据库有了数据,也会因为缓存中的空值而返回空,所以需要处理缓存空值的情况 因此,我们在设计接口时,需要考虑: - 缓存穿透:对查询不到的数据也进行缓存(缓存空对象或使用布隆过滤器) - 缓存更新:当数据更新时,需要同步更新缓存(这里先不考虑,只做基本的缓存入) 调整后的服务层逻辑: 1. 先查Redis,如果存在则直接返回 2. 如果Redis中没有,则查询数据库 - 如果数据库中存在,则入Redis并设置过期时间 - 如果数据库中不存在,则缓存一个空对象(或特殊标记)并设置较短的过期时间(防止缓存穿透) 下面我们按照这个思路调整代码: ### 1. 服务层调整 ```java @Service public class UserService { private static final String USER_KEY_PREFIX = "user:"; private static final long CACHE_NULL_TTL = 5; // 空值缓存时间(分钟) private final UserRepository userRepository; private final RedisTemplate<String, Object> redisTemplate; @Autowired public UserService(UserRepository userRepository, RedisTemplate<String, Object> redisTemplate) { this.userRepository = userRepository; this.redisTemplate = redisTemplate; } public User getUserAndCache(Long id) { String redisKey = USER_KEY_PREFIX + id; // 1. 从Redis获取 User cachedUser = (User) redisTemplate.opsForValue().get(redisKey); if (cachedUser != null) { // 判断是否是空对象(我们约定如果为空,则缓存一个特定的空对象,这里用null表示,但实际缓存了一个空对象) // 注意:我们也可以缓存一个特殊对象来表示空,但这里为了简单,我们直接返回null // 但这样无法区分是缓存了空还是真的有数据。所以我们可以用一个特殊对象,比如new User()(但这样不好),或者用Optional.empty(),但序列化问题。 // 另一种做法:单独设置一个空标识,比如在key上做标记,但这样复杂。所以我们采用缓存一个空对象,并设置较短的过期时间。 // 这里我们约定:如果从缓存取出的对象是null,则说明之前缓存了空(但Redis的value为null实际上不会存储,所以我们用另一个方式:缓存一个特殊字符串) // 调整:我们不用null,而是用一个特定的字符串,比如"NULL_OBJECT" // 但这样就需要在序列化时注意。我们这里简化:如果缓存命中且是null,则返回null(实际上Redis不会存储null值,所以这里cachedUser为null表示缓存未命中) // 因此,我们这里还是按照之前的方式,但需要处理数据库查不到的情况。 return cachedUser; // 如果缓存的是空对象,这里cachedUser就是null,但这样和没缓存一样,所以需要改进 } // 2. 这里需要改进:上面的写法无法区分缓存了空对象和未缓存。所以我们需要改变策略: // 方案一:使用两个key,一个存数据,一个存是否存在。这样复杂。 // 方案二:缓存一个特殊对象(比如一个空User对象,但这样不好,因为无法区分),或者用一个特定的类。 // 方案三:使用redis的String类型,当数据库不存在时,存入一个特定的字符串(比如"null")并设置过期时间,然后接口返回null。下次再来时,如果取到"null",则直接返回null。 // 但这样就需要在取值时判断类型。所以我们可以: // 重新设计: // 从Redis获取,如果得到的是字符串"null",表示数据库没有,返回null // 如果得到的是User对象,则返回User对象 // 如果都没有,则查询数据库 // 但这样我们使用了两种类型,不好处理。所以我们可以统一用User类型,但是当数据库没有时,我们缓存一个特定的User对象(比如id=-1)来表示不存在,但这样侵入业务。 // 更常见的做法:当数据库查询不存在时,缓存一个空值(比如一个空字符串或特殊字符串)并设置较短过期时间。 // 我们这里采用:缓存空字符串,并设置较短过期时间(5分钟) // 修改代码:我们不再直接使用User类型,而是使用Object。但这样不方便,所以我们换一种方式: // 实际我们之前的代码已经可以,但无法避免缓存穿透。所以我们需要在数据库中不存在时,也缓存一个值(比如null)并设置过期时间,但是Redis不能存储null值。 // 因此,我们使用一个占位对象: Object value = redisTemplate.opsForValue().get(redisKey); if (value != null) { if (value instanceof User) { return (User) value; } else if ("NULL_OBJECT".equals(value)) { // 如果是空对象占位符,则返回null return null; } } // 3. 缓存中没有,查询数据库 User dbUser = userRepository.findById(id).orElse(null); // 4. 数据库不存在 if (dbUser == null) { // 缓存空对象,防止缓存穿透 redisTemplate.opsForValue().set(redisKey, "NULL_OBJECT", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } // 5. 数据库存在,入缓存 redisTemplate.opsForValue().set(redisKey, dbUser, 1, TimeUnit.HOURS); return dbUser; } } ``` 但是上述代码在序列化时可能会出现问题,因为同一个key可能存储不同类型(User或String)。所以我们需要确保序列化器支持多种类型。 ### 2. 使用通用序列化器 在Redis配置中,我们使用了`GenericJackson2JsonRedisSerializer`,它可以将对象序列化为JSON,并且包含类型信息。这样当我们反序列化时,可以还原成正确的类型。 但是,如果我们存储一个字符串,它会序列化为JSON字符串(带引号),而存储User对象则是一个JSON对象。所以在反序列化时,我们需要根据类型信息来转换。 但是`GenericJackson2JsonRedisSerializer`已经可以处理,我们在服务层取出来时,如果是字符串,它会被反序列化成String类型,如果是User对象,则反序列化成User对象。 所以上面的代码可以工作,但要注意:我们存储的`"NULL_OBJECT"`会被序列化为一个字符串,而User对象会被序列化为一个JSON对象。 ### 3. 控制器层不变 ```java @RestController @RequestMapping("/users") public class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @GetMapping("/{id}") public ResponseEntity<User> getUserById(@PathVariable Long id) { User user = userService.getUserAndCache(id); if (user != null) { return ResponseEntity.ok(user); } return ResponseEntity.notFound().build(); } } ``` ### 4. 测试 - 测试存在的数据:`/users/1` -> 返回用户数据,并入Redis - 测试不存在的数据:`/users/9999` -> 第一次查询数据库返回404,同时Redis中会存入一个表示空值的字符串(5分钟过期)。在5分钟内再次查询,不会查询数据库,直接返回404。 ### 5. 改进点 - 使用布隆过滤器(Bloom Filter)来防止缓存穿透(如引用[2]所述),但需要预加载所有id,对于动态新增的数据不友好。所以这里我们采用缓存空值的方式。 - 当数据更新时,需要删除缓存(这里没有实现,只做基本缓存入)。 ### 完整代码示例(服务层改进版) ```java @Service public class UserService { private static final String USER_KEY_PREFIX = "user:"; private static final long CACHE_NULL_TTL = 5; // 空值缓存时间(分钟) private final UserRepository userRepository; private final RedisTemplate<String, Object> redisTemplate; @Autowired public UserService(UserRepository userRepository, RedisTemplate<String, Object> redisTemplate) { this.userRepository = userRepository; this.redisTemplate = redisTemplate; } public User getUserAndCache(Long id) { String redisKey = USER_KEY_PREFIX + id; // 1. 从Redis获取 Object value = redisTemplate.opsForValue().get(redisKey); if (value != null) { // 判断是否是空对象 if (value instanceof User) { return (User) value; } else if (value instanceof String && "NULL_OBJECT".equals(value)) { return null; } } // 2. 缓存中没有,查询数据库 User dbUser = userRepository.findById(id).orElse(null); // 3. 数据库不存在 if (dbUser == null) { // 缓存空对象,防止缓存穿透 redisTemplate.opsForValue().set(redisKey, "NULL_OBJECT", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } // 4. 数据库存在,入缓存 redisTemplate.opsForValue().set(redisKey, dbUser, 1, TimeUnit.HOURS); return dbUser; } } ``` ### 配置类(确保使用GenericJackson2JsonRedisSerializer) ```java @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } } ``` ### 注意事项 - 空值缓存时间(5分钟)可以根据业务调整 - 当数据被创建后,如果之前缓存了空值,需要清除空值缓存(否则在5分钟内还是查不到新数据)。这里没有实现,后续可以通过发布事件等方式清除。 这样就实现了根据id查询数据并入Redis,同时防止了缓存穿透。 ### 相关问题 1. 如何更新Redis中的缓存数据? 2. 如何实现缓存与数据库的双一致性? 3. 除了缓存空对象,还有哪些防止缓存穿透的方法?</think>在Spring Boot中实现根据ID查询MySQL数据并入Redis的接口,可以按照以下步骤操作: ### 完整实现方案 #### 1. 添加依赖 (pom.xml) ```xml <dependencies> <!-- Web支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Redis支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- JPA支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- JSON序列化 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies> ``` #### 2. 配置数据库和Redis (application.yml) ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/your_db?useSSL=false username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true redis: host: 127.0.0.1 port: 6379 password: your_redis_password database: 0 ``` #### 3. 实体类 ```java @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; // 省略getter/setter } ``` #### 4. Repository接口 ```java @Repository public interface UserRepository extends JpaRepository<User, Long> { } ``` #### 5. 服务层实现 ```java @Service public class UserService { private static final String USER_KEY_PREFIX = "user:"; private static final long CACHE_NULL_TTL = 5; // 空值缓存时间(分钟) @Autowired private UserRepository userRepository; @Autowired private RedisTemplate<String, Object> redisTemplate; public User getUserAndCache(Long id) { String redisKey = USER_KEY_PREFIX + id; // 1. 尝试从Redis获取 Object cached = redisTemplate.opsForValue().get(redisKey); if (cached instanceof User) { return (User) cached; } else if ("NULL_OBJECT".equals(cached)) { return null; // 防止缓存穿透[^2][^3] } // 2. Redis不存在则查询数据库 User user = userRepository.findById(id).orElse(null); // 3. 处理查询结果 if (user != null) { // 数据库存在:入Redis(1小时过期) redisTemplate.opsForValue().set(redisKey, user, 1, TimeUnit.HOURS); } else { // 数据库不存在:缓存空值(5分钟过期) redisTemplate.opsForValue().set(redisKey, "NULL_OBJECT", CACHE_NULL_TTL, TimeUnit.MINUTES); } return user; } } ``` #### 6. Redis配置类 ```java @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用JSON序列化 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); template.setValueSerializer(serializer); template.setKeySerializer(new StringRedisSerializer()); return template; } } ``` #### 7. 控制器层 ```java @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public ResponseEntity<?> getUserById(@PathVariable Long id) { User user = userService.getUserAndCache(id); return user != null ? ResponseEntity.ok(user) : ResponseEntity.status(HttpStatus.NOT_FOUND).body("用户不存在"); } } ``` ### 关键功能说明 1. **缓存策略**: - 先查Redis缓存,命中则直接返回 - 未命中时查询MySQL数据库 - 查询结果入Redis(存在数据或空值) 2. **防止缓存穿透**: - 当数据库不存在数据时,缓存特殊标记"NULL_OBJECT" - 设置较短过期时间(5分钟),避免无效请求冲击数据库[^2][^3] 3. **序列化配置**: - 使用JSON序列化保证数据结构可读性 - 支持复杂对象存储 4. **键设计规范**: - 使用`user:{id}`格式保证键唯一性 - 清晰区分不同业务数据 ### 测试示例 使用curl测试接口: ```bash # 查询存在的用户 curl http://localhost:8080/api/users/1 # 查询不存在的用户 curl http://localhost:8080/api/users/9999 ``` ### 验证Redis数据 ```bash redis-cli > KEYS user:* 1) "user:1" > GET user:1 "{\"@class\":\"com.example.model.User\",\"id\":1,\"name\":\"张三\",\"email\":\"zhang@example.com\"}" ``` ### 性能优化建议 1. **批量操作**:当需要处理批量ID时,可使用`multiGet`批量获取缓存 2. **连接池配置**:在application.yml中添加连接池配置 ```yaml spring: redis: lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 ``` 3. **热点数据**:对高频访问数据可延长过期时间 4. **缓存更新**:添加数据更新时的缓存删除逻辑 ### 注意事项 1. 数据库与缓存一致性:更新数据时需要同步清理缓存 2. 空值缓存时间不宜过长,建议5-30分钟 3. 生产环境建议添加限流措施防止恶意请求
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值