使用asentinel-orm在Java中映射PostgreSQL JSON数据类型

本文详细介绍了如何使用asentinel-orm框架在Java应用中映射PostgreSQL的JSON数据类型,包括实体配置、转换服务实现和完整的读写操作示例,帮助开发者高效处理JSON数据存储。

使用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类型列时成为绝佳选择,而且对于实现应用程序的数据访问层也是如此,因此我真心推荐您尝试一下。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计