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);
    }
}

至于删除操作,虽然我们未定义外键级联操作,但基于聚合根的设计理念,关联集合数据仍会被删除。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。