Spring Data JDBC
Spring Data JDBC是Spring Data系列中的一款轻量级数据访问框架,它不像JPA/Hibernate那样提供完整的全功能ORM,而是基于直接基于JDBC实现了简单的对象和数据表映射。Spring Data JDBC非常适合数据导入导出、ETL等数据处理任务场景,这类场景的特点是数据规模较大,SQL的查询逻辑则较为简单。但Spring Data JDBC对动态SQL的支持极为有限,因此通常不适用于实现复杂业务系统。
引入Maven依赖
在SpringBoot工程中集成Spring Data JDBC非常简单,我们直接引入起步依赖即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
除此之外我们还需要数据库驱动,这里以MySQL5.7版本为例进行介绍。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
配置数据源
在SpringBoot工程的application.properties
配置文件中,我们需要配置数据库连接相关信息。
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
使用Spring Data JDBC增删改查
这里我们介绍Spring Data JDBC的基础使用方式。
定义数据模型类
Spring Data JDBC实现了对象和数据的映射,在Java代码中,我们需要使用Spring Data注解标注类和字段。注意这里需要使用Spring Data注解而非JPA注解,这是因为Spring Data JDBC并非基于JPA实现的,它们之间完全没有依赖关系。
这里我们的表结构如下。
CREATE TABLE `t_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL,
`password` varchar(20) DEFAULT NULL,
`birthday` datetime DEFAULT NULL,
PRIMARY KEY (`user_id`)
);
对应的数据模型类定义如下。
package com.gacfox.demo.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table("t_user")
public class User {
@Id
@Column("user_id")
private Long userId;
@Column("username")
private String username;
@Column("password")
private String password;
@Column("birthday")
private Date birthday;
}
@Table:指定表名,如果不指定,默认使用类名的小写形式作为表名。
@Id:指定该字段为主键字段,Spring Data JDBC要求实体类必须有一个主键字段,否则无法进行正常的插入或更新操作。
@Column:指定字段对应的数据表列名,如果不指定,默认字段名与数据库列名一致。
虽然@Table
和@Column
具有默认值,但我们一般都建议在这里显式指名,尽量避免依赖其默认行为。
简单增删改查
Spring Data JDBC实现了类似Spring Data JPA的Repository操作接口,CrudRepository
是Spring Data JDBC的核心接口,它用于实现基础的增删改查功能,我们自定义的接口需要继承这个接口类型。CrudRepository
中包含了增删改查的默认方法,Spring启动后会自动创建其实例,我们不必手动实现该接口。
UserRepository.java
package com.gacfox.demo.repository;
import com.gacfox.demo.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
}
下面代码中,我们调用前面创建的Repository实现增删改查。
UserService.java
package com.gacfox.demo.service;
import com.gacfox.demo.model.User;
public interface UserService {
void saveUser(User user);
void updateUser(User user);
void deleteUser(Long id);
User getUser(Long id);
}
UserServiceImpl.java
package com.gacfox.demo.service;
import com.gacfox.demo.model.User;
import com.gacfox.demo.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service("userService")
@Transactional(rollbackFor = Exception.class)
public class UserServiceImpl implements UserService {
@Resource
private UserRepository userRepository;
@Override
public void saveUser(User user) {
userRepository.save(user);
}
@Override
public void updateUser(User user) {
userRepository.save(user);
}
@Override
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
@Override
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
}
其中,Repository的save()
会在主键字段被设置时更新,而在主键未设置时插入,这也是Spring Data JDBC要求实体类必须有主键字段的原因。注意这和Spring Data JPA不同,一旦设置主键值,Spring Data JDBC就会认为这个实体已经存在于数据库中,所以它执行的是UPDATE
操作而不是INSERT
操作。
批量插入
Spring Data JDBC支持saveAll()
方法,具体来说在数据库驱动支持时,多个插入可以被合并为一条INSERT语句,这样比逐条执行INSERT语句效率高很多。
@Override
public void batchSaveUser(List<User> users) {
userRepository.saveAll(users);
}
注:如果数据库驱动不支持批量插入,它底层可能还是逐条插入的。
使用方法名规则查询
Spring Data JDBC支持findBy、existsBy、countBy、deleteBy等方法命名规则,以findBy为例,我们可以在Repository中按如下规则定义方法名,实现多条件查询逻辑。
findBy<属性名>[操作符][逻辑连接词]<属性名>[操作符]...
下面例子中,我们定义了根据用户名查询用户的查询逻辑。
package com.gacfox.demo.repository;
import com.gacfox.demo.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findByUsername(String username);
}
注意,我们同样不必实现该方法,Spring Data JDBC会自动按findBy方法命名规则解析这个方法名,调用时会自动生成对应的SQL。对于其它的方法名前缀、操作符等其实我们不必背下来,它们都符合大致的英文语义,使用时根据IDE提示构建即可。此外,对于方法名规则查询也不太推荐用于复杂的查询逻辑,这可能导致方法名过长,代码可读性下降。
执行自定义SQL语句
除了使用Repository自带的方法和方法名规则查询,我们还可以在Repository中声明执行自定义SQL语句的方法,下面是一个例子。
package com.gacfox.demo.repository;
import com.gacfox.demo.model.User;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
@Query("SELECT * FROM t_user WHERE username = :username")
Optional<User> findByUsername(@Param("username") String username);
}
在Repository中,要执行的SQL需要使用@Query
注解指定,参数则使用@Param
注解标注,其中SQL的参数使用:参数名
占位符形式表达。
聚合根(Aggregate Root)
Spring Data JDBC并不支持传统的类似JPA的实体类关联关系定义,但它支持聚合根(Aggregate Root)模式,所谓的聚合根表示当前实体包含一个从属集合,这些从属元素将被持久化为单独的表并关联到父实体。当实体类对象被保存时,关联的集合默认会被全部清空并重新插入,聚合根保存时是全量更新的,这与Spring Data JPA不同。
这里我们准备t_user
用户表和t_group
用户组表两张数据表,它们具有一对多关系,通过t_user.group_id
作为外键关联字段(不过这里我们不真的创建外键约束)。
CREATE TABLE `t_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT,
`group_id` bigint(20) DEFAULT NULL,
`username` varchar(20) DEFAULT NULL,
`password` varchar(20) DEFAULT NULL,
`birthday` datetime DEFAULT NULL,
PRIMARY KEY (`user_id`)
);
CREATE TABLE `t_group` (
`group_id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
PRIMARY KEY (`group_id`)
);
对应的数据模型实体类如下。
User.java
package com.gacfox.demo.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table("t_user")
public class User {
@Id
@Column("user_id")
private Long userId;
@Column("group_id")
private Long groupId;
@Column("username")
private String username;
@Column("password")
private String password;
@Column("birthday")
private Date birthday;
}
Group.java
package com.gacfox.demo.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.data.relational.core.mapping.Table;
import java.util.Set;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table("t_group")
public class Group {
@Id
@Column("group_id")
private Long groupId;
@Column("name")
private String name;
@MappedCollection(idColumn = "group_id")
private Set<User> users;
}
代码中,我们使用了@MappedCollection
注解定义聚合根,其中idColumn
参数指定关联的外键字段名。
当我们组装好Group
实例并保存时,它关联的User
实例也会被插入。
package com.gacfox.demo.service;
import com.gacfox.demo.model.Group;
import com.gacfox.demo.repository.GroupRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service("groupService")
@Transactional(rollbackFor = Exception.class)
public class GroupServiceImpl implements GroupService{
@Resource
private GroupRepository groupRepository;
@Override
public void saveGroup(Group group) {
groupRepository.save(group);
}
}
至于删除操作,虽然我们未定义外键级联操作,但基于聚合根的设计理念,关联集合数据仍会被删除。