Spring Boot中如何使用JDBC读取和写入数据,JDBC和JPA的对比,JdbcTemplate和SimpleJdbcInsert的用法对比

JDBC和JPA的对比

  • JDBC(Java Database Connectivity)提供一种接口,它是由各种数据库厂商提供类和接口组成的数据库驱动,为多种数据库提供统一访问。我们使用数据库时只需要调用JDBC接口就行了。

    JDBC的用途:与数据库建立连接、发送 操作数据库的语句并处理结果。

    JDBC示意图
    在这里插入图片描述

  • JPA(Java Peisitence API)是Java持久层API。它是对java应用程序访问ORM(对象关系映射)框架的规范。为了我们能用相同的方法使用各种ORM框架。

    JPA用途:简化现有Java EE和Java SE应用开发工作;整合ORM技术。

    使用JPA只需要创建实体(这和创建一个POJO(Plain Ordinary Java Object)简单的Java对象一样简单),用@entity进行注解。在Spring Data JPA中,定义一个简单的接口,用于把对象持久化到数据库的仓库。

    常见ORM框架:Hibernate。由于MyBatis需要手写SQL,所以不完全属于ORM框架,而Hibernate则完全不需要手写SQL。

    JPA示意图
    在这里插入图片描述
    不同点:

  1. 使用的sql语言不同:

    JDBC使用的是基于关系型数据库的标准SQL语言;

    JPA通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。

  2. 操作的对象不同:

    JDBC操作的是数据,将数据通过SQL语句直接传送到数据库中执行:

    JPA操作的是持久化对象,由底层持久化对象的数据更新到数据库中。

  3. 数据状态不同:

    JDBC操作的数据是“瞬时”的,变量的值无法与数据库中的值保持一致;

    JPA操作的数据时可持久的,即持久化对象的数据属性的值是可以跟数据库中的值保持一致的。

Spring Boot中使用JDBC读取和写入数据

Spring对JDBC的支持主要在于JdbcTemplate

JdbcTemplate类中主要有如下方法

  • batchUpdate(...)//批量更新

  • execute(...)//执行SQL语句

  • query(...)//查询并返回相应值

  • queryForList(...)//查询并返回一个List

  • queryForObject(...)//查询并返回一个Object

  • queryForMap(...)//查询并返回一个Map

  • queryForRowSet(...)//查询并返回一个RowSet

  • update(...)//执行一条插入或更新语句

使用JdbcTemplate查询数据库的例子

JdbcTemplate中的queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args)方法是将查询得到的结果映射为一个Object。

public Ingredient findById(String id) {
    return jdbc.queryForObject(
        "select id, name, type from Ingredient where id=?",
        this::mapRowToIngredient, id);
}

//mapRowToIngredient方法,ResultSet是查询返回的结果集
private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
    throws SQLException {
    return new Ingredient(
        rs.getString("id"), 
        rs.getString("name"),
        Ingredient.Type.valueOf(rs.getString("type")));
}

/****************************	等效于	 ************************************/
public Ingredient findById(String id) {
    return jdbc.queryForObject(
        "select id, name, type from Ingredient where id=?",
        new RowMapper<Ingredient>() {
            public Ingredient mapRow(ResultSet rs, int rowNum) 
                throws SQLException {
                return new Ingredient(
                    rs.getString("id"), 
                    rs.getString("name"),
                    Ingredient.Type.valueOf(rs.getString("type")));
            };
        }, id);
}

findById方法中调用的queryForObject方法中需要传入一个RowMapper的实例

@Override
@Nullable
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException {
    List<T> results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1));
    return DataAccessUtils.nullableSingleResult(results);
}

RowMapper<T>接口中只有一个T mapRow(ResultSet rs, int rowNum) throws SQLException抽象方法。需要一个子类来继承RowMapper并实现mapRow方法。如果使用Lambda,则只需要传入相应所需执行的代码。

@FunctionalInterface
public interface RowMapper<T> {
    
	@Nullable
	T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
  • ResultSet是查询数据库所返回的结果集。

  • mapRow方法是将查询所得到数据库的一行映射为一个对象,也就是将ResultSet的第一行映射为一个Object。

对Lambda表达式不熟悉的可以移步我的另一篇博文:
Java中Lambda对比匿名内部类

使用JdbcTemplate的步骤

调整对象

一般来说,为了将对象持久化到数据库中需要增加idcreatedTime字段,id一般都设置为自增,由数据库自动生成。同时需要为每个实体类增加get,set方法。如果使用了Lombok,只需要添加@Data注解,就会在运行时自动为对象增加上get和set方法,从而避免了手写get和set方法时的繁琐。

import java.util.Date;
import java.util.List;

import lombok.Data;

@Data
public class Taco {

  private Long id;
  private Date createdAt;
  ...
}

导入依赖

首先需要Jdbc的依赖

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

然后需要配置数据库

如下给出了H2数据库和MySQL的配置示例

配置H2数据库

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- 引入spring-boot-devtools的目的是在运行时可以访问H2数据库 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
</dependency>

如果需要初始化数据库,特别是H2数据库。

可以在resourse文件夹下新建两个sql文件:schema.sqldata.sql

  • scheaml.sql中的SQL用于初始化数据库,比如创建表。

  • data.sql中的SQL语句用来插入数据

默认的访问地址如下:

http://localhost:8080/h2-console/

默认的JDBC的连接为:jdbc:h2:mem:testdb,默认的用户名为sa,密码为空
Spring Boot控制台打印的日志显示了连接H2数据库的JDBC URL。
在这里插入图片描述
H2数据库访问页面
在这里插入图片描述

配置MySQL

mysql-connector得根据数据库的版本来选择,8.0的数据库就得选择版本为8或者以上的mysql-connector

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>

然后在application.properties中配置连接相关的信息

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456

定义JDBC Repository

比如有一个Ingredient的对象对应着数据库中Ingredient的表,其属性分别对应着数据库中(id, name, type)这几个字段。我们需要定义如下方法:

  1. 从数据中查询所有的Ingredient的信息,并将其保存到一个Ingredient的集合中
  2. 根据id查询单个Ingredient
  3. 保存Ingredient对象到数据库中

首先需要定义IngredientRepository的interface

package tacos.data;
import tacos.Ingredient;

public interface IngredientRepository {

  Iterable<Ingredient> findAll();
  
  Ingredient findById(String id);
  
  Ingredient save(Ingredient ingredient);
  
}

然后需要使用JdbcTemplate来具体实现这个接口

package tacos.data;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import tacos.Ingredient;

@Repository//为JdbcIngredientRepository定义了@Repository以后,Spring扫描到这个类时,就会将其初始化为Spring上下文中的一个Bean
public class JdbcIngredientRepository implements IngredientRepository {

  private JdbcTemplate jdbc;
  
  //只要我们在pom.xml中导入JDBC的依赖,Spring Boot就会为我们自动配置一个JdbcTemplate的Bean,
  //我们只需要将这个Bean注入到我们的代码中
  @Autowired
  public JdbcIngredientRepository(JdbcTemplate jdbc) {
    this.jdbc = jdbc;
  }

  @Override
  public Iterable<Ingredient> findAll() {
    return jdbc.query("select id, name, type from Ingredient",
        this::mapRowToIngredient);
  }

  @Override
  public Ingredient findById(String id) {
    return jdbc.queryForObject(
        "select id, name, type from Ingredient where id=?",
        this::mapRowToIngredient, id);
  }

  @Override
  public Ingredient save(Ingredient ingredient) {
    jdbc.update(
        "insert into Ingredient (id, name, type) values (?, ?, ?)",
        ingredient.getId(), 
        ingredient.getName(),
        ingredient.getType().toString());
    return ingredient;
  }

  private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
      throws SQLException {
    return new Ingredient(
        rs.getString("id"), 
        rs.getString("name"),
        Ingredient.Type.valueOf(rs.getString("type")));
  }
}

在Controller中注入和使用repository

package tacos.web;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;

import tacos.Ingredient;
import tacos.Ingredient.Type;
import tacos.data.IngredientRepository;

@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {
  
  private final IngredientRepository ingredientRepo;

  //IngredientRepository使用了@Repository注解,IngredientRepository的Bean就会被注册到Spring的上下文中,因此这里只需注入即可
  @Autowired
  public DesignTacoController(IngredientRepository ingredientRepo) {
    this.ingredientRepo = ingredientRepo;
  }

  @GetMapping
  public String showDesignForm(Model model) {
    List<Ingredient> ingredients = new ArrayList<>();
    ingredientRepo.findAll().forEach(i -> ingredients.add(i));
    
    Type[] types = Ingredient.Type.values();
    for (Type type : types) {
      model.addAttribute(type.toString().toLowerCase(), 
          filterByType(ingredients, type));      
    }
    return "design";
  }

  private List<Ingredient> filterByType(List<Ingredient> ingredients, Type type) {
    return ingredients
              .stream()
              .filter(x -> x.getType().equals(type))
              .collect(Collectors.toList());
  }
}

插入数据

到此JdbcTemplate的基本使用就到此结束,下面将介绍SimpleJdbcInsert。对比JdbcTemplate,它的功能更强大些,插入数据也更加方便

插入一行数据可以直接使用JdbcTemplate中的update方法。

但是考虑到多表关联的情况,使用JdbcTemplate就有些麻烦:

如下所示:有三张表,一个用户可以创建多个订单,所以user_order表中一个user可以对应多条order。

我们在创建订单的时候,除了需要将该订单插入order表中,还需要将order_id插入到user_order表中。通常来说id字段都是自增的,我们只需要往order表中插入order_name以及createTime就会自动为该记录生成一个order_id。插入成功以后,我们需要取出该记录的order_id,与user_id一起插入到user_order表中。
在这里插入图片描述
对比两段代码来看看在实现方法上两者的差别

代码中相应的表的字段以及相互关系如下:
在这里插入图片描述

Taco对应Java对象有如下属性:
在这里插入图片描述
如下代码的作用都是分别将order中的信息分别插入到Taco表和Taco_Ingredients表

使用JdbcTemplate进行插入

package tacos.data;

import java.sql.Timestamp;
import java.sql.Types;
import java.util.Arrays;
import java.util.Date;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;

import tacos.Ingredient;
import tacos.Taco;

@Repository
public class JdbcTacoRepository implements TacoRepository {

  private JdbcTemplate jdbc;

  public JdbcTacoRepository(JdbcTemplate jdbc) {
    this.jdbc = jdbc;
  }

  @Override
  public Taco save(Taco taco) {
    long tacoId = saveTacoInfo(taco);
    taco.setId(tacoId);
    for (Ingredient ingredient : taco.getIngredients()) {
      saveIngredientToTaco(ingredient, tacoId);
    }

    return taco;
  }

  private long saveTacoInfo(Taco taco) {
    taco.setCreatedAt(new Date());
    PreparedStatementCreator psc =
        new PreparedStatementCreatorFactory(
            "insert into Taco (name, createdAt) values (?, ?)",
            Types.VARCHAR, Types.TIMESTAMP
        ).newPreparedStatementCreator(
            Arrays.asList(
                taco.getName(),
                new Timestamp(taco.getCreatedAt().getTime())));

    KeyHolder keyHolder = new GeneratedKeyHolder();
    jdbc.update(psc, keyHolder);

    return keyHolder.getKey().longValue();
  }

  private void saveIngredientToTaco(
          Ingredient ingredient, long tacoId) {
    jdbc.update(
        "insert into Taco_Ingredients (taco, ingredient) " +
        "values (?, ?)",
        tacoId, ingredient.getId());
  }
}

使用SimpleJdbcInsert进行插入

package tacos.data;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import org.springframework.stereotype.Repository;
import tacos.Ingredient;
import tacos.Taco;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Repository
public class JdbcTacoRepository implements TacoRepository {

  private SimpleJdbcInsert tacoInserter;
  private SimpleJdbcInsert tacoIngredientsInserter;

  @Autowired
  public JdbcTacoRepository(JdbcTemplate jdbc) {
    this.tacoInserter = new SimpleJdbcInsert(jdbc)
            .withTableName("Taco")
            .usingGeneratedKeyColumns("id");
    this.tacoIngredientsInserter = new SimpleJdbcInsert(jdbc)
            .withTableName("Taco_Ingredients");
  }

  @Override
  public Taco save(Taco taco) {
    long tacoId = saveTacoInfo(taco);
    taco.setId(tacoId);
    for (Ingredient ingredient : taco.getIngredients()) {
      saveIngredientToTaco(ingredient, tacoId);
    }
    return taco;
  }

  private void saveIngredientToTaco(Ingredient ingredient, long tacoId) {
    Map<String, Object> values = new HashMap<>();
    values.put("taco", tacoId);
    values.put("ingredient", ingredient.getId());
    tacoIngredientsInserter.execute(values);
  }

  private long saveTacoInfo(Taco taco) {
    taco.setCreatedAt(new Date());
    Map<String, Object> values = new HashMap<>();
    values.put("createdAt", taco.getCreatedAt());
    values.put("name", taco.getName());
    long tacoId = tacoInserter
            .executeAndReturnKey(values)
            .longValue();
    return tacoId;
  }
}

对比SimpleJdbcInsert和JdbcTemplate的使用

左边是使用JdbcTemplate。为了得到插入数据以后生成的tacoId。需要使用PreparedStatementCreatorkeyHolder。相比之下,SimpleJdbcInsert的代码则要简洁很多。
在这里插入图片描述

参考

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页