使用asentinel-orm在Java中映射PostgreSQL JSON数据类型
在软件产品开发中,经常需要直接高效地管理和存储JSON内容到底层数据库。本文将通过asentinel-orm(一个基于Spring JDBC的轻量级ORM工具)来演示如何便捷地完成此类任务。
我们将首先定义一个包含JSONB列的简单实体,然后配置一个使用asentinel-orm访问PostgreSQL数据库的示例应用,最后展示如何正确查询和存储实际的JSON数据。
环境设置
- Java 21
- Spring Boot 3.5.6
- PostgreSQL驱动版本42.7.7
- asentinel-orm版本1.71.2
实现步骤
假设PostgreSQL数据库服务器已启动并运行,首先创建简单模式:
1
|
create schema articles;
|
考虑实体代表一篇文章,由id和code明确描述,其余属性以JSON格式保存:
1
2
3
4
5
|
CREATE TABLE IF NOT EXISTS articles (
id SERIAL PRIMARY KEY,
code VARCHAR NOT NULL UNIQUE,
attributes JSONB NOT NULL
);
|
假设attributes的表示如下:
1
2
3
4
5
|
{
"title": "How to map JSON columns with asentinel-orm",
"author": "Horatiu Dan",
"words": 1154
}
|
为了使用ORM注解并将映射应用到Java实体,需要在pom.xml文件中添加特定依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.asentinel.common</groupId>
<artifactId>asentinel-common</artifactId>
<version>1.71.2</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
|
同时,为了与PostgreSQL数据库交互,包含了postgresql驱动依赖和spring-boot-starter-jdbc,以确保自动DataSource配置。由于涉及JSON处理,Jackson依赖也存在,并在项目配置中构建和添加了映射器实例。
现在可以创建对应的Java实体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Table("Articles")
public class Article {
@PkColumn("id")
private int id;
@Column("code")
private String code;
@Column(value = "attributes", sqlParam = @SqlParam("jsonb"))
private Attributes attributes;
// ... 其他代码
}
|
注解说明:
@Table - 将数据库表与类关联,在生成自动SQL查询时使用
@PkColumn - 将表主键与指定属性关联
@Column - 将指定表列与对应的Java属性关联
需要特别关注@Column注解的sqlParam属性,其目的是在注解成员映射特殊列或用户定义列时,提供有关映射数据库列类型的信息。这里显然是JSONB(非标准SQL类型),因此使用@SqlParam注解来指示。后文将看到它与ConversionService结合使用,以正确处理Java类型和SQL类型之间的自定义转换。
本文的目的是映射PostgreSQL JSON列并执行双向操作。这需要自定义数据类型转换,因此必须构建ConversionService并注入到DefaultEntityDescriptorTreeRepository(用于从数据库读取)和SimpleUpdater(用于写入数据库)中。
构建DefaultEntityDescriptorTreeRepository和OrmOperations实例的方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@Bean
public DefaultEntityDescriptorTreeRepository entityDescriptorTreeRepository(SqlBuilderFactory sqlBuilderFactory,
@Qualifier("ormConversionService") ConversionService conversionService) {
DefaultEntityDescriptorTreeRepository treeRepository = new DefaultEntityDescriptorTreeRepository();
treeRepository.setSqlBuilderFactory(sqlBuilderFactory);
treeRepository.setConversionService(conversionService);
return treeRepository;
}
@Bean
public OrmOperations orm(JdbcFlavor jdbcFlavor,
SqlQuery sqlQuery,
SqlBuilderFactory sqlBuilderFactory,
@Qualifier("ormConversionService") ConversionService conversionService) {
SimpleUpdater updater = new SimpleUpdater(jdbcFlavor, sqlQuery);
updater.setConversionService(conversionService);
return new OrmTemplate(sqlBuilderFactory, updater);
}
@Bean("ormConversionService")
public ConversionService ormConversionService() {
GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverter(new JsonToObjectConverter());
conversionService.addConverter(new ObjectToJsonConverter());
return conversionService;
}
|
此外,应将适当的转换器添加到配置中,并向ConversionService注册,现在它已增强能够分别转换为/从JSONB:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
private static final ObjectMapper MAPPER = JsonMapper.builder().build();
private static class JsonToObjectConverter implements ConditionalGenericConverter {
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
PGobject pgObj = (PGobject) source;
try {
return MAPPER.readValue(pgObj.getValue(), targetType.getType());
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to convert from JSON.", e);
}
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (!(sourceType.getType() == PGobject.class)) {
return false;
}
Column column = targetType.getAnnotation(Column.class);
if (column == null) {
return false;
}
return "jsonb".equals(column.sqlParam().value());
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return null;
}
}
private static class ObjectToJsonConverter implements ConditionalGenericConverter {
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
String s;
try {
s = MAPPER.writeValueAsString(source);
PGobject pgo = new PGobject();
pgo.setType("jsonb");
pgo.setValue(s);
return pgo;
} catch (JsonProcessingException | SQLException e) {
throw new IllegalArgumentException("Failed to convert to JSON.", e);
}
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (!(targetType instanceof SqlParameterTypeDescriptor)) {
return false;
}
SqlParameterTypeDescriptor typeDescriptor = (SqlParameterTypeDescriptor) targetType;
return "jsonb".equals(typeDescriptor.getTypeName());
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return null;
}
}
|
在之前创建的Article实体类中,使用了@SqlParam注解,并将jsonb数据库类型名称作为其值。它基本上为Attributes注解字段触发ConversionService,并导致上面声明的2个转换器用于读取和写入该字段。
此示例应用的ORM完整配置是OrmConfig。
完成后,映射PostgreSQL JSON列就很简单了。由于Article类中的attributes字段已按上述方式注解,让我们构建一个服务,允许实际将此类记录写入和读取到数据库中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Service
public class ArticleService {
private final OrmOperations orm;
public ArticleService(OrmOperations orm) {
this.orm = orm;
}
@Transactional
public void write(Article article) {
orm.update(article);
}
@Transactional
public Optional<Article> readByCode(String code) {
return orm.newSqlBuilder(Article.class)
.select()
.where().column("code").eq(code)
.execForOptional();
}
}
|
所有交互都通过OrmOperations实例完成。前一个方法将Article保存到数据库中,而后一个方法通过其唯一code读取一个。就这样简单,不需要其他步骤。
此外,为了验证实现,运行以下测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@SpringBootTest
@Transactional
class ArticleServiceTest {
@Autowired
private ArticleService articleService;
@Test
void manageArticles() {
var code = UUID.randomUUID().toString();
var attributes = new Attributes("Technically-correct Article", "Horatiu Dan", 1200);
var article = new Article(code, attributes);
articleService.write(article);
Optional<Article> read = articleService.readByCode(code);
Assertions.assertTrue(read.isPresent());
final Article readArticle = read.get();
Assertions.assertEquals(code, readArticle.getCode());
final Attributes readAttributes = readArticle.getAttributes();
Assertions.assertEquals(attributes.getTitle(), readAttributes.getTitle());
Assertions.assertEquals(attributes.getAuthor(), readAttributes.getAuthor());
Assertions.assertEquals(attributes.getWords(), readAttributes.getWords());
}
}
|
最后,为了探索目的,将上述测试转换为非事务性测试(实际项目中不推荐)。首次运行后,articles数据库表的内容如下所示,并描绘了目标表示。attributes列存储表示为JSON的数据:
1
2
3
4
5
|
+--+------------------------------------+--------------------------------------------------------------------------------+
|id|code |attributes |
+--+------------------------------------+--------------------------------------------------------------------------------+
|5 |10f594cf-90ba-4280-b6d8-9308ab16916a|{"title": "Technically-correct Article", "words": 1200, "author": "Horatiu Dan"}|
+--+------------------------------------+--------------------------------------------------------------------------------+
|
结论
在本文中,我们演示了如何使用asentinel-orm开源项目映射数据库JSON列。尽管这里选择的底层数据库是PostgreSQL,但同样可以在其他数据库(如MySQL、Oracle、H2等)上类似地完成。另一方面,即使有人可能发现初始ORM配置在开始时有点困难,需要一些ORM理解,但一旦完成,其使用就相当简单直观,更不用说出色的ORM性能了。
考虑到从1.71.0版本开始,asentinel-orm通过利用Spring框架ConversionService,允许添加从Java到数据库类型及反之的自定义转换功能,这不仅使其在处理JSONB类型列时成为绝佳选择,而且对于实现应用程序的数据访问层也是如此,因此我真心推荐您尝试一下。