3.1使用JDBC读写数据

3.1使用JDBC读写数据

几十年来,关系数据库和SQL一直是数据持久化的首选。尽管近年来出现了许多替代数据库类型,但关系数据库仍然是通用数据存储的首选,而且不太可能很快被取代。

在处理关系数据时,Java开发人员有多个选择。两个最常见的选择是JDBCJPASpring通过抽象支持这两种方式,这使得使用JDBCJPA比不使用Spring更容易。在本节中,我们将重点讨论Spring是如何支持JDBC的,然后在第3.2节中讨论SpringJPA的支持。

Spring JDBC支持起源于JdbcTemplate类。JdbcTemplate提供了一种方法,通过这种方法,开发人员可以对关系数据库执行SQL操作,与通常使用JDBC不同的是,这里不需要满足所有的条件和样板代码。

为了更好地理解JdbcTemplate的作用,我们首先来看一个示例,看看如何在没有JdbcTemplate的情况下用Java执行一个简单的查询。程序清单3.1不使用JdbcTemplate查询数据库

@Override
public Ingredient findOne(String id) {
    Connection connection = null;
    PreparedStatement statement = null;
    ResultSet resultSet = null;
    try {
        connection = dataSource.getConnection();
        statement = connection.prepareStatement(
            "select id, name, type from Ingredient");
        statement.setString(1, id);
        resultSet = statement.executeQuery();
        Ingredient ingredient = null;
        if(resultSet.next()) {
            ingredient = new Ingredient(
                resultSet.getString("id"),
                resultSet.getString("name"),
                Ingredient.Type.valueOf(resultSet.getString("type")));
        }
        return ingredient;
    } catch (SQLException e) {
        // ??? What should be done here ???
    } finally {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
            }
        }
    }
    return null;
}

在程序清单3.1的某个地方,有几行代码用于查询数据库中的ingredients。但是很难在JDBC的混乱代码中找到查询指针。它被创建连接、创建语句和通过关闭连接、语句和结果集来清理的代码所包围。

更糟糕的是,在创建连接或语句或执行查询时,可能会出现许多问题。这要求捕获一个SQLException,这可能有助于(也可能无助于)找出问题出在哪里或如何解决问题。

SQLException是一个被检查的异常,它需要在catch块中进行处理。但是最常见的问题,如未能创建到数据库的连接或输入错误的查询,不可能在catch块中得到解决,可能会重新向上抛出以求处理。相反,要是考虑使用JdbcTemplate的方法。程序清单3.2使用JdbcTemplate查询数据库

private JdbcTemplate jdbc;

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

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

程序清单3.2中的代码显然比程序清单3.1中的原始JDBC示例简单得多;没有创建任何语句或连接。而且,在方法完成之后,不会对那些对象进行任何清理。最后,这样做不会存在任何在catch块中不能处理的异常。剩下的代码只专注于执行查询(调用JdbcTemplatequeryForObject()方法)并将结果映射到Ingredient对象(在mapRowToIngredient()方法中

程序清单3.2中的代码是使用JdbcTemplateTaco Cloud应用程序中持久化和读取数据所需要做的工作的一个片段。让我们采取下一步必要的步骤来为应用程序配备JDBC持久话。我们将首先对域对象进行一些调整。

3.1.1为域适配持久化

在将对象持久化到数据库时,通常最好有一个惟一标识对象的字段。Ingredient类已经有一个id字段,但是需要向TacoOrder添加id字段。

此外,了解何时创建Taco以及何时放置Order可能很有用。还需要向每个对象添加一个字段,以捕获保存对象的日期和时间。下面的程序清单显示了Taco类中需要的新idcreatedAt字段。程序清单3.3Taco类添加idtimestamp字段

@Data
public class Taco {

    private Long id;

    private Date createdAt;

    ...
}

因为使用Lombok在运行时自动生成访问器方法,所以除了声明idcreatedAt属性外,不需要做任何事情。它们将在运行时根据需要生成适当的gettersetter方法。Order类也需要做类似的修改,如下所示:

@Data
public class Order {

    private Long id;

    private Date placedAt;

    ...
}

同样,Lombok会自动生成访问字段的方法,因此只需要按顺序进行这些更改(如果由于某种原因选择不使用Lombok,那么需要自己编写这些方法

域类现在已经为持久化做好了准备。让我们看看如何使用JdbcTemplate在数据中对它们进行读写。

3.1.2使用JdbcTemplate

在开始使用JdbcTemplate之前,需要将它添加到项目类路径中。这很容易通过添加Spring BootJDBC starter依赖来实现:

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

还需要一个存储数据的数据库。出于开发目的,嵌入式数据库也可以。我喜欢H2嵌入式数据库,所以我添加了以下依赖进行构建:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

稍后,将看到如何配置应用程序来使用外部数据库。但是现在,让我们继续编写一个获取和保存Ingredient数据的存储库。

定义JDBC存储库

Ingredient repository需要执行以下操作:

  • 查询所有的Ingredient使之变成一个Ingredient的集合对象
  • 通过它的id查询单个Ingredient
  • 保存一个Ingredient对象

以下IngredientRepository接口将这三种操作定义为方法声明:

package tacos.data;

import tacos.Ingredient;

public interface IngredientRepository {

    Iterable<Ingredient> findAll();

    Ingredient findOne(String id);

    Ingredient save(Ingredient ingredient);
}

尽管该接口体现了需要Ingredient repository做的事情的本质,但是仍然需要编写一个使用JdbcTemplate来查询数据库的IngredientRepository的实现。下面的程序清单是编写实现的第一步。程序清单3.4使用JdbcTemplate开始编写Ingredient repository

package tacos.data;

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
public class JdbcIngredientRepository implements IngredientRepository {

    private JdbcTemplate jdbc;

    @Autowired
    public JdbcIngredientRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    ...

}

JdbcIngredientRepository使用@Repository进行了注解。这个注解是Spring定义的少数几个原型注解之一,包括@Controller@Component。通过使用@RepositoryJdbcIngredientRepository进行注解,这样它就会由Spring组件在扫描时自动发现,并在Spring应用程序上下文中生成bean实例。

Spring创建JdbcIngredientRepository bean时,通过@Autowired注解将JdbcTemplate注入到bean中。构造函数将JdbcTemplate分配给一个实例变量,该变量将在其他方法中用于查询和插入数据库。谈到那些其他方法,让我们来看看findAll()findById()的实现。程序清单3.5使用JdbcTemplate查询数据库

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

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

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

findAll()findById()都以类似的方式使用JdbcTemplate。期望返回对象集合的findAll()方法使用了JdbcTemplatequery()方法。query()方法接受查询的SQL以及SpringRowMapper实现,以便将结果集中的每一行映射到一个对象。findAll()还接受查询中所需的所有参数的列表作为它的最后一个参数。但是,在本例中,没有任何必需的参数。

findById()方法只期望返回单个成分对象,因此它使用JdbcTemplatequeryForObject()方法而不是query()queryForObject()的工作原理与query()非常相似,只是它返回的是单个对象,而不是对象列表。在本例中,它给出了要执行的查询、一个RowMapper和要获取的Ingredientid,后者用于代替查询SQL中 的 ?

如程序清单3.5所示,findAll()findById()RowMapper参数作为mapRowToIngredient()方法的方法引用。当使用JdbcTemplate作为显式RowMapper实现的替代方案时,使用Java 8的方法引用和lambda非常方便。但是,如果出于某种原因,想要或是需要一个显式的RowMapper,那么findAll()的以下实现将展示如何做到这一点:

@Override
public Ingredient findOne(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);
}

从数据库读取数据只是问题的一部分。在某些情况下,必须将数据写入数据库以便能够读取。因此,让我们来看看如何实现save()方法。

插入一行

JdbcTemplateupdate()方法可用于在数据库中写入或更新数据的任何查询。并且,如下面的程序清单所示,它可以用来将数据插入数据库。程序清单3.6使用JdbcTemplate插入数据

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

因为没有必要将ResultSet数据映射到对象,所以update()方法要比query()queryForObject()简单得多。它只需要一个包含SQL的字符串来执行,以及为任何查询参数赋值。在本例中,查询有三个参数,它们对应于save()方法的最后三个参数,提供了Ingredientidnametype

完成了JdbcIngredientRepository后,现在可以将其注入到DesignTacoController中,并使用它来提供一个Ingredient对象列表,而不是使用硬编码的值(正如第2章中所做的那样DesignTacoController的变化如下所示。程序清单3.7在控制器中注入并使用repository

@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {

    private final IngredientRepository ingredientRepo;

    @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";
    }

    ...

}

请注意,showDesignForm()方法的第2行现在调用了注入的IngredientRepositoryfindAll()方法。findAll()方法从数据库中提取所有Ingredient,然后将它们对应到到模型的不同类型中。

几乎已经准备好启动应用程序并尝试这些更改了。但是在开始从查询中引用的Ingredient表读取数据之前,可能应该创建这个表并写一些Ingredient数据进去。

3.1.3定义模式并预加载数据

除了Ingredient表之外,还需要一些保存订单和设计信息的表。图3.1说明了需要的表以及这些表之间的关系。

![3.1 Taco Cloud数据表](E:\Document\spring-in-action-v5-translate\第一部分Spring基础\第三章 处理数据\3.1 Taco Cloud数据表.jpg)

3.1 Taco Cloud数据表

3.1中的表有以下用途:

  • Ingredient -保存着原料信息
  • Taco -保存着关于taco设计的重要信息
  • Taco_Ingredient -包含Taco表中每一行的一个或多行数据,将Taco映射到该TacoIngredient
  • Taco_Order -保存着重要的订单细节
  • Taco_Order_Tacos -包含Taco_Order表中的每一行的一个或多行数据,将Order映射到Order中的Tacos

下一个程序清单显示了创建表的SQL语句。程序清单3.8定义Taco Cloud模式

create table if not exists Ingredient (
    id varchar(4) not null,
    name varchar(25) not null,
    type varchar(10) not null
);

create table if not exists Taco (
    id identity,
    name varchar(50) not null,
    createdAt timestamp not null
);

create table if not exists Taco_Ingredients (
    taco bigint not null,
    ingredient varchar(4) not null
);

alter table Taco_Ingredients add foreign key (taco) references Taco(id);
alter table Taco_Ingredients add foreign key (ingredient) references Ingredient(id);

create table if not exists Taco_Order (
    id identity,
    deliveryName varchar(50) not null,
    deliveryStreet varchar(50) not null,
    deliveryCity varchar(50) not null,
    deliveryState varchar(2) not null,
    deliveryZip varchar(10) not null,
    ccNumber varchar(16) not null,
    ccExpiration varchar(5) not null,
    ccCVV varchar(3) not null,
    placedAt timestamp not null
);

create table if not exists Taco_Order_Tacos (
    tacoOrder bigint not null,
    taco bigint not null
);

alter table Taco_Order_Tacos add foreign key (tacoOrder) references Taco_Order(id);
alter table Taco_Order_Tacos add foreign key (taco) references Taco(id);

最大的问题是把这个模式定义放在哪里。事实证明,Spring Boot回答了这个问题。

如果有一个名为schema.sql的文件。在应用程序的类路径根目录下执行sql,然后在应用程序启动时对数据库执行该文件中的SQL。因此,应该将程序清单3.8的内容写入一个名为schema.sql的文件中,然后放在项目的src/main/resources文件夹下。

还需要用一些Ingredient数据来预加载数据库。幸运的是,Spring Boot还将执行一个名为data.sql的文件,这个文件位于根路径下。因此,可以使用src/main/resources/data.sql中的下面程序清单中的insert语句来加载包含Ingredient数据的数据库。程序清单3.9预加载数据库

delete from Taco_Order_Tacos;
delete from Taco_Ingredients;
delete from Taco;
delete from Taco_Order;
delete from Ingredient;

insert into Ingredient (id, name, type) values ('FLTO', 'Flour Tortilla', 'WRAP');
insert into Ingredient (id, name, type) values ('COTO', 'Corn Tortilla', 'WRAP');
insert into Ingredient (id, name, type) values ('GRBF', 'Ground Beef', 'PROTEIN');
insert into Ingredient (id, name, type) values ('CARN', 'Carnitas', 'PROTEIN');
insert into Ingredient (id, name, type) values ('TMTO', 'Diced Tomatoes', 'VEGGIES');
insert into Ingredient (id, name, type) values ('LETC', 'Lettuce', 'VEGGIES');
insert into Ingredient (id, name, type) values ('CHED', 'Cheddar', 'CHEESE');
insert into Ingredient (id, name, type) values ('JACK', 'Monterrey Jack', 'CHEESE');
insert into Ingredient (id, name, type) values ('SLSA', 'Salsa', 'SAUCE');
insert into Ingredient (id, name, type) values ('SRCR', 'Sour Cream', 'SAUCE');

即使只开发了Ingredient数据的存储库,也可以启动Taco Cloud应用程序并访问设计页面,查看JdbcIngredientRepository的运行情况。继续……试试吧。当回到代码中时,可以继续编写用于持久化TacoOrder的存储库和相应的数据。

3.1.4插入数据

到此,已经了解了如何使用JdbcTemplate向数据库写入数据。JdbcIngredientRepository中的save()方法使用JdbcTemplateupdate()方法将Ingredient对象保存到数据库中。

虽然这是第一个很好的例子,但是它可能有点太简单了。保存数据可能比JdbcIngredientRepository所需要的更复杂。使用JdbcTemplate保存数据的两种方法包括:

  • 直接使用update()方法
  • 使用SimpleJdbcInsert包装类

让我们首先看看,当持久话需求比保存一个Ingredient所需要的更复杂时,如何使用update()方法。

使用JdbcTemplate保存数据

目前,TacoOrder存储库需要做的惟一一件事是保存它们各自的对象。为了保存Taco对象,TacoRepository声明了一个save()方法,如下所示:

package tacos.data;

import tacos.Taco;

public interface TacoRepository {
    Taco save(Taco design);
}

类似地,OrderRepository也声明了一个save()方法:

package tacos.data;

import tacos.Order;

public interface OrderRepository {
    Order save(Order order);
}

看起来很简单,对吧?没那么快。保存一个Taco设计需要将与该Taco关联的Ingredient保存到Taco_Ingredient表中。同样,保存Order也需要将与Order关联的Taco保存到Taco_Order_Tacos表中。这使得保存TacoOrder比 保存Ingredient更有挑战性。

要实现TacoRepository,需要一个save()方法,该方法首先保存基本的Taco设计细节(例如,名称和创建时间,然后为Taco对象中的每个IngredientTaco_Ingredients中插入一行。下面的程序清单显示了完整的JdbcTacoRepository类。程序清单3.10使用JdbcTemplate实现TacoRepository

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

save()方法首先调用私有的saveTacoInfo()方法,然后使用该方法返回的Taco id调用saveIngredientToTaco(),它保存每个成分。关键在于saveTacoInfo()的细节。

Taco中插入一行时,需要知道数据库生成的id,以便在每个Ingredient中引用它。保存Ingrendient数据时使用的update()方法不能获得生成的id,因此这里需要一个不同的update()方法。

需要的update()方法接受PreparedStatementCreatorKeyHolderKeyHolder将提供生成的Taco id,但是为了使用它,还必须创建一个PreparedStatementCreator

如程序清单3.10所示,创建PreparedStatementCreator非常重要。首先创建一个PreparedStatementCreatorFactory,为它提供想要执行的SQL,以及每个查询参数的类型。然后在该工厂上调用newPreparedStatementCreator(),在查询参数中传递所需的值以生成PreparedStatementCreator

通过使用PreparedStatementCreator,可以调用update(),传入PreparedStatementCreatorKeyHolder(在本例中是GeneratedKeyHolder实例update()完成后,可以通过返回keyHolder.getKey().longValue()来返回Taco id

回到save()方法,循环遍历Taco中的每个成分,调用saveIngredientToTaco()方法。saveIngredientToTaco()方法使用更简单的update()形式来保存对Taco_Ingredient表引用。

TacoRepository剩下所要做的就是将它注入到DesignTacoController中,并在保存Taco时使用它。下面的程序清单显示了注入存储库所需的改变。程序清单3.11注入并使用TacoRepository

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

    @Autowired
    public DesignTacoController(
        IngredientRepository ingredientRepo,
        TacoRepository designRepo) {
        this.ingredientRepo = ingredientRepo;
        this.designRepo = designRepo;
    }

    ...

}

构造函数包含一个IngredientRepository和一个TacoRepository。它将这两个变量都赋值给实例变量,以便它们可以在showDesignForm()processDesign()方法中使用。

说到processDesign()方法,它的更改比showDesignForm()所做的更改要广泛一些。下一个程序清单显示了新的processDesign()方法。程序清单3.12保存Taco设计并链接到Order

@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {

    @ModelAttribute(name = "order")
    public Order order() {
        return new Order();
    }

    @ModelAttribute(name = "taco")
    public Taco taco() {
        return new Taco();
    }

    @PostMapping
    public String processDesign(
        @Valid Taco design, Errors errors,
        @ModelAttribute Order order) {

        if (errors.hasErrors()) {
            return "design";
        }

        Taco saved = designRepo.save(design);
        order.addDesign(saved);

        return "redirect:/orders/current";
    }
    ...
}

关于程序清单3.12中的代码,首先注意到的是DesignTacoController现在使用@SessionAttributes(“order”)进行了注解,并且在order()方法上有一个新的注解@ModelAttribute。与taco()方法一样,order()方法上的@ModelAttribute注解确保在模型中能够创建Order对象。但是与session中的Taco对象不同,这里需要在多个请求间显示订单,因此可以创建多个Taco并将它们添加到订单中。类级别的@SessionAttributes注解指定了任何模型对象,比如应该保存在会话中的order属性,并且可以跨多个请求使用。

taco设计的实际处理发生在processDesign()方法中,除了TacoErrors对象外,该方法现在还接受Order对象作为参数。Order参数使用@ModelAttribute进行注解,以指示其值应该来自模型,而Spring MVC不应该试图给它绑定请求参数。

在检查验证错误之后,processDesign()使用注入的TacoRepository来保存Taco。然后,它将Taco对象添加到保存于sessionOrder对象中。

实际上,Order对象仍然保留在session中,直到用户完成并提交Order表单才会保存到数据库中。此时,OrderController需要调用OrderRepository的实现来保存订单。我们来写一下这个实现。

使用SimpleJdbcInsert插入数据

保存一个taco不仅要将taco的名称和创建时间保存到Taco表中,还要将与taco相关的配料的引用保存到Taco_Ingredient表中。对于这个操作还需要知道Tacoid,这是使用KeyHolderPreparedStatementCreator来获得的。

在保存订单方面,也存在类似的情况。不仅必须将订单数据保存到Taco_Order表中,还必须引用Taco_Order_Tacos表中的每个taco。但是不是使用繁琐的PreparedStatementCreator,而是使用SimpleJdbcInsertSimpleJdbcInsert是一个包装了JdbcTemplate的对象,它让向表插入数据的操作变得更容易。

首先创建一个JdbcOrderRepository,它是OrderRepository的一个实现。但是在编写save()方法实现之前,让我们先关注构造函数,在构造函数中,将创建两个SimpleJdbcInsert实例,用于将值插入Taco_OrderTaco_Order_Tacos表中。下面的程序清单显示了JdbcOrderRepository(没有save()方法。程序清单3.13JdbcTemplate创建一个SimpleJdbcTemplate

package tacos.data;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import com.fasterxml.jackson.databind.ObjectMapper;

import tacos.Taco;
import tacos.Order;

@Repository
public class JdbcOrderRepository implements OrderRepository {

    private SimpleJdbcInsert orderInserter;
    private SimpleJdbcInsert orderTacoInserter;
    private ObjectMapper objectMapper;

    @Autowired
    public JdbcOrderRepository(JdbcTemplate jdbc) {
        this.orderInserter = new SimpleJdbcInsert(jdbc)
            .withTableName("Taco_Order")
            .usingGeneratedKeyColumns("id");

        this.orderTacoInserter = new SimpleJdbcInsert(jdbc)
            .withTableName("Taco_Order_Tacos");

        this.objectMapper = new ObjectMapper();
    }
    ...
}

JdbcTacoRepository一样,JdbcOrderRepository也通过其构造函数注入了JdbcTemplate。但是,构造函数并没有将JdbcTemplate直接分配给一个实例变量,而是使用它来构造两个SimpleJdbcInsert实例。

第一个实例被分配给orderInserter实例变量,它被配置为使用Taco_Order表,并假定id属性将由数据库提供或生成。分配给orderTacoInserter的第二个实例被配置为使用Taco_Order_Tacos表,但是没有声明如何在该表中生成任何id

构造函数还创建ObjectMapper实例,并将其分配给实例变量。尽管Jackson用于JSON处理,但稍后将看到如何重新使用它来帮助保存订单及其关联的tacos

现在让我们看看save()方法如何使用SimpleJdbcInsert实例。下一个程序清单显示了save()方法,以及几个用于实际工作的save()委托的私有方法。程序清单3.14使用SimpleJdbcInsert插入数据

@Override
public Order save(Order order) {
    order.setPlacedAt(new Date());
    long orderId = saveOrderDetails(order);
    order.setId(orderId);

    List<Taco> tacos = order.getTacos();
    for (Taco taco : tacos) {
        saveTacoToOrder(taco, orderId);
    }

    return order;
}

private long saveOrderDetails(Order order) {
    @SuppressWarnings("unchecked")
    Map<String, Object> values = objectMapper.convertValue(order, Map.class);
    values.put("placedAt", order.getPlacedAt());

    long orderId = orderInserter.executeAndReturnKey(values).longValue();

    return orderId;
}

private void saveTacoToOrder(Taco taco, long orderId) {
    Map<String, Object> values = new HashMap<>();
    values.put("tacoOrder", orderId);
    values.put("taco", taco.getId());

    orderTacoInserter.execute(values);
}

save()方法实际上并不保存任何东西。它定义了保存订单及其关联Taco对象的流,并将持久性工作委托给saveOrderDetails()saveTacoToOrder()

SimpleJdbcInsert有两个执行插入的有用方法:execute()executeAndReturnKey()。两者都接受Map<String, Object>,其中Map键对应于数据插入的表中的列名,映射的值被插入到这些列中。

通过将Order中的值复制到Map的条目中,很容易创建这样的Map。但是Order有几个属性,这些属性和它们要进入的列有相同的名字。因此,在saveOrderDetails()中,我决定使用JacksonObjectMapper及其convertValue()方法将Order转换为Map。这是必要的,否则ObjectMapper会将Date属性转换为long,这与Taco_Order表中的placedAt字段不兼容。

随着Map中填充完成订单数据,我们可以在orderInserter上调用executeAndReturnKey()方法了。这会将订单信息保存到Taco_Order表中,并将数据库生成的id作为一个Number对象返回,调用longValue()方法将其转换为从方法返回的long值。

saveTacoToOrder()方法要简单得多。不是使用ObjectMapper将对象转换为Map,而是创建Map并设置适当的值。同样,映射键对应于表中的列名。对orderTacoInserterexecute()方法的简单调用就能执行插入操作。

现在可以将OrderRepository注入到OrderController中并开始使用它。下面的程序清单显示了完整的OrderController,包括因使用注入的OrderRepository而做的更改。程序清单3.15OrderController中使用OrderRepository

package tacos.web;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import tacos.Order;
import tacos.data.OrderRepository;

@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
public class OrderController {

    private OrderRepository orderRepo;

    public OrderController(OrderRepository orderRepo) {
        this.orderRepo = orderRepo;
    }

    @GetMapping("/current")
    public String orderForm() {
        return "orderForm";
    }

    @PostMapping
    public String processOrder(@Valid Order order, Errors errors,
                               SessionStatus sessionStatus) {
        if (errors.hasErrors()) {
            return "orderForm";
        }

        orderRepo.save(order);
        sessionStatus.setComplete();
        return "redirect:/";
    }
}

除了将OrderRepository注入控制器之外,OrderController中惟一重要的更改是processOrder()方法。在这里,表单中提交的Order对象(恰好也是在session中维护的Order对象)通过注入的OrderRepository上的save()方法保存。

一旦订单被保存,就不再需要它存在于session中了。事实上,如果不清除它,订单将保持在session中,包括其关联的tacos,下一个订单将从旧订单中包含的任何tacos开始。因此需要processOrder()方法请求SessionStatus参数并调用其setComplete()方法来重置会话。

所有的JDBC持久化代码都准备好了。现在,可以启动Taco Cloud应用程序并进行测试。你想要多少tacos和多少orders都可以。

可能还会发现在数据库中进行挖掘是很有帮助的。因为使用H2作为嵌入式数据库,而且Spring Boot DevTools已经就位,所以应该能够用浏览器访问http://localhost:8080/h2-console来查看H2控制台。虽然需要确保JDBC URL字段被设置为JDBC:h2:mem:testdb,但是默认的凭证应该可以让你进入。登录后,应该能够对Taco Cloud模式中的表发起查询。

SpringJdbcTemplateSimpleJdbcInsert使得使用关系数据库比普通JDBC简单得多。但是可能会发现JPA使它更加简单。让我们回顾一下之前的工作,看看如何使用Spring数据使数据持久化更加容易。

下一页