第8章 离不开的数据库

前面讲了这么多,都没有涉及到数据的存储。现在的软件系统多多少少都会涉及到数据库的存储。不管做什么,App、web、C/S客户端软件,都需要将涉及到的数据存储起来,一般来说,目前最常用的数据存储方式还是关系型数据库。

本章我们就一起来看下在以Spring Boot为基础的项目中,如何方便地操作数据库。

Spring Boot应用中访问数据库的方式有多种。常用的有下面几种:

  • JdbcTemplate
  • Spring Data JPA
  • Mybatis集成(本文不涉及,后续再写)

8.1 集成JdbcTemplate

JdbcTemplate是Spring
Boot提供的一个数据操作组件,封装了基本的数据库操作,本节主要针对常用数据库mysql来展示如何使用JdbcTemplate进行数据操作。

首先,在pom文件中添加JdbcTemplate和mysql的支持。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

接下来,在application.properties中添加数据源配置项。

    spring.datasource.url=jdbc:mysql://localhost:3306/m_customer
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.main.allow-bean-definition-overriding=true

新增类UserService,在其中直接注入JdbcTemplate使用。

    package cn.com.hanbinit.customer.service;
    
    import cn.com.hanbinit.customer.model.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.stereotype.Service;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.List;
    
    @Service
    public class UserService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
    * 创建用户
    * @param nickname
    * @param age
    */
    public Boolean create(String nickname, Integer age){
        int number = jdbcTemplate.update("insert into tb_user(nickname, age) values
        (?,?)", nickname, age);
        if(number == 1){
            return true;
        }
        return false;
    }
    
    /**
    * 根据用户Id删除单个用户
    * @param userId
    * @return
    */
    public Boolean deleteById(Integer userId){
        int number = jdbcTemplate.update("delete from tb_user where id = ?", userId);
        if(number == 1){
            return true;
        }
        return false;
    }
    
    /**
    * 根据Id获取单个用户信息
    * @param userId
    * @return
    */
    public User getUserById(Integer userId){
        return jdbcTemplate.queryForObject("select id, nickname, age from tb_user where
        id = ?", (resultSet, i) -> {
            User user = new User();
            user.setId(resultSet.getInt("id"));
            user.setNickname(resultSet.getString("nickname"));
            user.setAge(resultSet.getInt("age"));
            return user;
        }, userId);
    }
    
    /**
    * 获取所有用户列表
    * @return
    */
    public List<User> getAllUsers(){
    
        List<User> userList = jdbcTemplate.query("select *from tb_user", (resultSet,
        i) -> {
            User user = new User();
            user.setId(resultSet.getInt("id"));
            user.setNickname(resultSet.getString("nickname"));
            user.setAge(resultSet.getInt("age"));
            return user;
        });
        return userList;
        }
    }

其中使用到的User类,代码如下:

    package cn.com.hanbinit.customer.model;
    
    /**
    
    * 用户基本信息
      */
      public class User {
    
        private Integer id;
        private String nickname;
        private Integer age;
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getNickname() {
            return nickname;
        }
    
        public void setNickname(String nickname) {
          this.nickname = nickname;
        }
    
        public Integer getAge() {
          return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
      }

最后,将UserService注入到UserController中,就可以直接使用了。

    @Autowired
    private UserService userService;
    
    ……
    
    @GetMapping("/users/{userId}")
    public User getUserInfoByUserId(@PathVariable Integer userId){
        return userService.getUserById(userId);
    }
    
    @PostMapping("/users")
    public Boolean saveUser(@RequestBody User user){
        return userService.create(user.getNickname(), user.getAge());
    }
    
    @GetMapping("/users")
    public List<User> getAllUsers(){
        return userService.getAllUsers();
    }
    
    @DeleteMapping("/users/{userId}")
    public Boolean deleteUserById(@PathVariable Integer userId){
        return userService.deleteById(userId);
    }

UserController省略了部分之前已经存在的代码。

以上的代码操作了数据库中的tb_user表,表结构如下:

    CREATE TABLE `tb_user` (
        `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
        `nickname` varchar(50) DEFAULT NULL,
        `age` int(3) DEFAULT NULL,
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

通过图8.1-8.3可以看出来,我们通过POST来向数据库中添加数据。

图8.1 调用接口,向数据库中谈添加数据

图8.2 查询单个用户信息

图8.3 删除Id=2的用户

在查询所有的用户列表,可以看到,id=2的数据已经被删除掉,如图8.4所示。

图8.4 用户列表查询

8.2 集成Spring Data JPA

JPA相传是为了整合第三方ORM框架,建立统一持久化标准而来的。在大名鼎鼎的Hibernate中,JPA的使用整合地就相当完美。JPA本身是一系列的接口,Hibernate实现了JPA的接口后,今天我们要介绍的Spring
Data JPA,其实也可以简单理解为Spring实现了JPA的接口。

实际项目开发中,大部分的数据库操作都是相对简单的单表“增删改差”。正常情况下,开发人员需要写许多额外的代码来实现很多的基本数据操作。Spring
Data
JPA提供了最基本的JpaRepository接口,继承这个接口就可以实现绝大多数的数据库操作。

下面我们改造一下order微服务,添加基本订单表,使用Spring Data
JPA实现数据表的简单操作。

首先在pom.xml中添加Spring Data JPA的引用。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

在application.properties中添加如下配置项。

    spring.datasource.url=jdbc:mysql://localhost:3306/m_order
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.main.allow-bean-definition-overriding=true
    spring.jpa.properties.hibernate.hbm2ddl.auto=create-drop

其中,spring.jpa.properties.hibernate.hbm2ddl.auto是hibernate的配置属性,其主要作用是:自动创建、更新、验证数据库表结构。该参数的几种配置如下:

  • create:每次加载hibernate时都会删除上一次的生成的表,然后根据你的model类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。
  • create-drop:每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
  • update:最常用的属性,第一次加载hibernate时根据model类会自动建立起表的结构(前提是先建立好数据库),以后加载hibernate时根据model类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等应用第一次运行起来后才会。
  • validate:每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。

添加model包,新建类Order,如下代码,指定了接下来需要操作的实体对象。

    package cn.com.hanbinit.order.model;
    
    import javax.persistence.*;
    import java.util.Date;
    
    // @Entity 指明这个类是一个可以持久化的对象
    @Entity
    // @Table 指明了本类对应的数据库中表的信息
    @Table(name = "tb_order")
    public class Order {
    
        // @Id修饰的变量是主键
        @Id
        // @GeneratedValue 是一个简单的主键生成策略,在Mysql中指的是Auto-Increment
        @GeneratedValue
        private Long id;
    
        private String title;
    
        // 指明成员变量createDate对应的表字段为create_date且不能为空
        @Column(name="create_date", nullable = false)
        private Date createDate;
    
        // 指明成员变量createBy对应的表字段为create_by
        @Column(name="create_by", nullable = false)
        private String createBy;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;    
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public Date getCreateDate() {
            return createDate;
        }
    
        public void setCreateDate(Date createDate) {
            this.createDate = new Date();
        }
    
        public String getCreateBy() {
            return createBy;
        }
    
        public void setCreateBy(String createBy) {
            this.createBy = createBy;
        }
    }

接下来,创建repository包,并新建接口OrderRepository继承JpaRepository。代码如下:

    package cn.com.hanbinit.order.repository;
    
    import cn.com.hanbinit.order.model.Order;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface OrderRepository extends JpaRepository<Order, Long> {
    
    }

上面代码中的JpaRepository接口是Jpa中简单的一个接口类,通过下面的代码可方便看到其中定义了很多的相对口语化的方法。

    @NoRepositoryBean
    public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T,
    ID>, QueryByExampleExecutor<T> {
    
        /*
        * (non-Javadoc)
        * @see org.springframework.data.repository.CrudRepository#findAll()
        */
        List<T> findAll();
    
        /*
        * (non-Javadoc)
        * @see
          org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
        */
        List<T> findAll(Sort sort);
    
        /*
        * (non-Javadoc)
        * @see
          org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable)
        */
        List<T> findAllById(Iterable<ID> ids);
    
        /*
        * (non-Javadoc)
        * @see
          org.springframework.data.repository.CrudRepository#save(java.lang.Iterable)
        */
        <S extends T> List<S> saveAll(Iterable<S> entities);
    
        /**
        * Flushes all pending changes to the database.
        */
        void flush();
    
        /**
        * Saves an entity and flushes changes instantly.
        *
        * @param entity
        * @return the saved entity
        */
        <S extends T> S saveAndFlush(S entity);
    
        /**
        * Deletes the given entities in a batch which means it will create a single
          {@link Query}. Assume that we will clear
        * the {@link javax.persistence.EntityManager} after the call.
        *
        * @param entities
        */
        void deleteInBatch(Iterable<T> entities);
    
        /**
        * Deletes all entities in a batch call.
        */
        void deleteAllInBatch();
    
        /**
        * Returns a reference to the entity with the given identifier.
        *
        * @param id must not be {@literal null}.
        * @return a reference to the entity with the given identifier.
        * @see EntityManager#getReference(Class, Object)
        * @throws javax.persistence.EntityNotFoundException if no entity exists for
          given {@code id}.
        */
        T getOne(ID id);
    
        /*
        * (non-Javadoc)
        * @see
          org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
        */
        @Override
        <S extends T> List<S> findAll(Example<S> example);
    
        /*
        * (non-Javadoc)
        *@see
        org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example,
        org.springframework.data.domain.Sort)
        */
        @Override
        <S extends T> List<S> findAll(Example<S> example, Sort sort);
    
    }

接口中定义的这些方法都是可以在继承了这个类的接口中直接使用的。使用代码:

    @Autowired
    private OrderRepository orderRepository;

将OrderRepository注入到OrderController中,并在OrderController中添加接口。

    /**
    * 获取所有的用户
    * @return
      */
      @GetMapping("/orders")
      public List<Order> queryAllOrders(){
      return orderRepository.findAll();
      }
    
    /**
    * 新增用户
    * @param order
    * @return
      */
      @PostMapping("/orders")
      public Order createOrder(@RequestBody Order order){
      order.setCreateDate(new Date());
      return orderRepository.save(order);
      }

启动order微服务,在数据库中可以看到已经自动创建表tb_order,使用desc
tb_order;查看表结构,可以看到如图8.5所示结果。

图8.5 自动创建表tb_order的结构

在postman中模拟调用上面新增的保存order信息接口,可以看到图8.6所示的结果。

图8.6 模拟调用使用Spring Data JPA实现的order保存功能

使用图8.6中的方式一次向数据库中插入多条数据,然后将请求的Method改为GET重新调用。可以看到图8.7中的结果。
图8.7 模拟调用使用Spring Data JPA实现的order列表查询功能

Spring Data JPA的上手难度比较低,对单表的操作也很方便,但是对于多表关联操作来说略显麻烦,接下来笔者会带大家一起了解下Spring Data JPA是如何处理多表关联操作情况的。

8.4 小结

本章通过为之前创建的微服务项目添加JdbcTemplate,Spring Data JPA支持,读者可以掌握到如何为Spring Boot项目添加关系型数据库的操作支持。