首先在 pom.xml 中引入依赖。
org.springframework.boot
spring-boot-starter-jdbc
com.h2database
h2
runtime
示例程序打算使用 h2 缓存数据库,所以这里也一并引用。
1 h2 缓存数据库
h2是一个开源的嵌入式(非嵌入式设备)数据库引擎,基于Java开发,可直接嵌入到应用程序中,与应用程序一起打包发布,不受平台限制。
启动应用后,在浏览器地址栏输入 http://127.0.0.1:8080/h2-console,就可以打开 h2 控制台。
首先选择控制台编码格式为中文,接着输入 JDBC URL,然后点击“测试连接”,如果连接成功,就会提示“测试成功”。
最后点击“连接”按钮,就会打开数据库控制台客户端,连接到 h2 数据库:
2 初始化表结构与数据
在 src/main/resources/ 下,新建 schema.sql 文件编写表结构 SQL。在同一个目录下,新建 data.sql 文件,编写初始化数据 SQL。这样在应用启动时,Spring Boot 就会执行这些脚本。
schema.sql:
create table if not exists Book
( id varchar( 4) not null, name varchar( 25) not null, type varchar( 10) not null );
data.sql:
insert into Book
(id, name, type)
values
('1', '两京十五日', '小说');
insert into Book
(id, name, type)
values
('2', '把自己作为方法', '历史');
insert into Book
(id, name, type)
values
('3', '正常人', '小说');
启动成功后,就会在 h2 数据库控制台客户端中看到新建好的表与数据。
点击左侧的 Book,就会在右侧的 SQL 输入框中自动生成查询该表的 SQL 语句,然后点击 “Run”,执行它。我们就会在右下角看到初始化的表数据。
3 编码
3.1 新建实体类
@Data
@RequiredArgsConstructor
public class Book {
private final String id;
private final String name;
private final String type;
}
这里用了 Lombok 插件。Lombok 是一种 Java 实用工具,可用来帮助我们消除 Java 冗长的样板式代码。
加了 @Data 注解的Java 类,在编译之后会自动为我们加上这些方法:
- 所有属性的get和set方法;
- toString 方法;
- hashCode方法;
- equals方法。
@RequiredArgsConstructor 注解会将类中所有带有 @NonNull 注解和以final修饰的未经初始化的字段作为构造函数的入参。
3.2 新建 Repository 类
首先定义一个 Repository 接口,然后新建这个接口的实现类。
接口:
public interface BookRepository {
Iterable findAll();
Book findOne(String id);
Book save(Book Book);
}
实现类:
@Repository
public class JdbcBookRepository implements BookRepository {
private JdbcTemplate jdbc;
@Autowired
public JdbcBookRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Override
public Iterable findAll() {
return null;
}
@Override
public Book findOne(String id) {
return null;
}
@Override
public Book save(Book Book) {
return null;
}
}
@Repository和@Controller、@Service、@Component的作用差不多,目的都是把对象交给spring管理。@Repository一般用在持久层的实现类上。
@Autowired注解可通过byType的形式,来给指定的字段或方法注入所需的外部资源。
autowire 有以下四种模式:
模式 | 说明 |
---|---|
byName | 根据属性的名字自动装配 |
byType | 根据属性的类型自动装配 |
constructor | 与 byType 类似,不同之处在于它应用于构造器参数,如果没有找到会抛出异常 |
autodetect | 会在 byType 和 constructor 中智能选择 |
这里通过 @Autowired 标注的构造器将 JdbcTemplate 注入进来。这个构造器将 JdbcTemplate 赋值给一个实例变量,这个变量会被其他方法用来执行数据库查询或更新等操作。
3.3 查询操作
假设我们需要查询出所有的书籍,那么就可以调用 JdbcTemplate 的 List
方法。
@Override
public Iterable findAll() {
return jdbc.query("select id, name, type from Book",
this::mapRowToBook);
}
private Book mapRowToBook(ResultSet rs, int rowNum) throws SQLException {
return new Book(rs.getString("id"), rs.getString("name"),
rs.getString("type"));
}
这里利用了 Java 的方法引用,来编写 RowMapper 入参。这样做的好处是:相对于原来的匿名内部类的写法,方法引用的写法更加简洁。
3.4 update()
JdbcTemplate 的 update() 方法可以用来新增或更新数据。
@Override
public Book save(Book book) {
jdbc.update("insert into Book (id,name,type) values (?,?,?)",
book.getId(),
book.getName(),
book.getType()
);
return book;
}
update() 方法定义如下:
int update(String sql, @Nullable Object... args)
它接受一个包含占位符的 SQL 语句以及多个入参。返回实际影响到的记录数。
建立 Spring 单元测试类来验证刚刚新建的 save() 方法:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class JdbcBookRepositoryTest {
@Autowired
private JdbcBookRepository jdbcBookRepository;
@Test
public void save() {
Book book = new Book("4", "比利时的哀愁", "小说");
jdbcBookRepository.save(book);
Book find=jdbcBookRepository.findOne("4");
assertEquals("比利时的哀愁",find.getName());
}
}
3.5 SimpleJdbcInsert 包装器类
SimpleJdbcInsert 一般用于多表插入场景。SimpleJdbcInsert 有两个方法执行数据插入操作: execute() 和 executeAndReturnKey()。 它们都接受 Map
作为参数,其中的 key 对应数据表中的列名,而 value 对应要插入到列中的实际值。
我们举一个图书示例。一本图书可以包含多个标签;而一个标签也可以隶属于多本图书。它们之间是多对多的关系,因此建立图书与标签的映射表来专门存放这些关系。具体如下图所示:
首先在 schema.sql 中,加入这些表结构创建语句:
create table if not exists Book
( id identity, name varchar( 25) not null, type varchar( 10) not null );
create table if not exists Tag
( id identity, name varchar( 25) not null);
create table if not exists Book_Tags ( book bigint not null, tag bigint not null );
alter table Book_Tags add foreign key (book) references Book( id);
alter table Book_Tags add foreign key (tag) references Tag( id);
接着,建立这些实体类:
@Data
@RequiredArgsConstructor
public class Tag {
private final Long id;
private final String name;
}
@Data
@RequiredArgsConstructor
public class Book {
private final String id;
private final String name;
private final String type;
private List tags = new ArrayList<>();
}
然后在 Repository 实现类的构造函数中,初始化每张表的 SimpleJdbcInsert 实例:
@Repository
public class JdbcBookRepository implements BookRepository {
private static final Logger log = LogManager.getFormatterLogger();
private final SimpleJdbcInsert bookInserter;
private final SimpleJdbcInsert tagInserter;
private final SimpleJdbcInsert bookTagsInserter;
private final ObjectMapper objectMapper;
private JdbcTemplate jdbc;
@Autowired
public JdbcBookRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
this.bookInserter = new SimpleJdbcInsert(jdbc)
.withTableName("Book")
.usingGeneratedKeyColumns("id");
this.tagInserter = new SimpleJdbcInsert(jdbc)
.withTableName("Tag")
.usingGeneratedKeyColumns("id");
this.bookTagsInserter = new SimpleJdbcInsert(jdbc)
.withTableName("Book_Tags");
this.objectMapper = new ObjectMapper();
}
...
}
SimpleJdbcInsert 的 withTableName() 方法用于指定表名;而 usingGeneratedKeyColumns() 方法用于指定主键。
具体保存操作代码为:
public Book saveIncludeTags(Book book) {
//保存图书
Map values = objectMapper.convertValue(book, Map.class);
long bookId = bookInserter.executeAndReturnKey(values).longValue();
//保存标签
List tagIds = new ArrayList<>();
for (Tag tag : book.getTags()) {
values = objectMapper.convertValue(tag, Map.class);
long tagId = tagInserter.executeAndReturnKey(values).longValue();
tagIds.add(tagId);
}
//关联图书与标签
for (Long tagId : tagIds) {
values.clear();
values.put("book", bookId);
values.put("tag", tagId);
log.info("values -> %s", values);
bookTagsInserter.execute(values);
}
return book;
}
- SimpleJdbcInsert 的 executeAndReturnKey() 与 execute() 方法都支持
Map
形式的入参。它们之间的区别是 executeAndReturnKey() 会返回 Number 形式的主键值。 - 可以利用 Jackson 的
ObjectMapper.convertValue(Object fromValue, Class
方法把一个 POJO 转换为相应的 Map 对象值。toValueType) - Number 类型可以根据场景对其进行转换。
这段代码首先先保存图书,得到图书主键;然后保存标签,得到标签主键;最后把前面得到的图书主键与标签主键保存到它们之间的关系表中。