如何在Java中将2D矩阵映射到Hibernate/JPA?

10

我有一个旧数据库,我正在尝试将其重新设计成21世纪的数据库。其中一种现有的数据结构涉及包含值的二维矩阵的特定类。如果我要从数据库中反向工程化这个类,我会得到一系列属性,如下所示:

private BigDecimal NODE_1_MATRIX_POS_1_1;
private BigDecimal NODE_1_MATRIX_POS_1_2;

等等,因为这是一个6x6的矩阵,所以有很多这样的列。

我一直在寻找更好的方法,但我不确定是否已经达到这个目标。我想要做的是像这样:

@Entity
public class TestClass {

    @Id
    private long id;

    @CollectionOfElements
    @JoinTable(
        name="MATRIX_DATA", 
        joinColumns=@JoinColumn(name="ENTRY_ID"))
    private List<List<BigDecimal>> matrix;

但是这会失败:

org.hibernate.MappingException: Could not determine type for: java.util.List, at table: MATRIX_DATA, for columns: [org.hibernate.mapping.Column(element)]

不仅仅是试图修复错误,我想询问并尝试找到解决这个映射挑战的正确 方法。有人通过 JPA 成功地映射多维数组吗?


我不确定最好的方法是什么,但我认为为了解决你的错误,你需要创建自己的UserType。 - 3urdoch
2个回答

12

我不仅仅想尝试修复错误,而是想询问并尝试找到解决这个映射挑战的正确方法。有人通过JPA成功地映射多维数组吗?

据我所知,标准JPA不支持嵌套集合。JPA维基书中有一个很好的章节涉及此主题(我仅引用其中的一部分):

嵌套集合、映射和矩阵

在对象模型中,复杂的集合关系很常见,例如一个List包含多个List(即矩阵),或者一个Map包含多个MapList等等。不幸的是,这些类型的集合在关系型数据库中映射效果非常差。

JPA不支持嵌套集合关系,并且通常最好更改您的对象模型以避免它们,从而使持久化和查询更容易。一种解决方案是创建一个包装嵌套集合的对象。

例如,如果Employee有一个Map,其中包含按String项目类型为键的Project列表,则可以创建一个新的ProjectType类来存储项目类型和一个OneToManyProject

...

这就是我的建议。例如:

@Entity
public class TestClass {    
    @Id
    private long id;

    @OneToMany(mappedBy="testClass")
    private List<MatrixRow> matrix;
}

在省略许多细节的情况下,MatrixLine 将会是:

@Entity
public class MatrixRow {
    @Id
    private long id;

    @ManyToOne
    private TestClass testClass;

    @CollectionOfElements
    private List<BigDecimal> row;
}

或者也许您可以使用自定义用户类型(我不太确定这将如何工作)。

或者(毕竟,您已经在使用非便携式注释),请查看此问题,以了解如何扩展Hibernate:


这正是我昨晚试图构建的东西。今天早上我会尝试一下,并结合你的示例中的差异。谢谢,Pascal! - Bret

5

Hibernate Types项目

您可以使用Hibernate Types项目来映射PostgreSQL多维数组。

您可以选择在实体属性方面使用Java数组或使用List

数据库表

例如,假设您有以下plane数据库表:

CREATE TABLE plane (
    id INT8 NOT NULL,
    name VARCHAR(255),
    seat_grid seat_status[][],
    PRIMARY KEY (id)
)

这里的seat_status是一个PostgreSQL枚举类型:

CREATE TYPE seat_status
AS ENUM (
    'UNRESERVED',
    'RESERVED',
    'BLOCKED'
);

JPA实体

您可以使用EnumArrayType映射seatGrid列:

@Entity(name = "Plane")
@Table(name = "plane")
@TypeDef(
    name = "seat_status_array",
    typeClass = EnumArrayType.class
)
public static class Plane {
 
    @Id
    private Long id;
 
    private String name;
 
    @Type(
        type = "seat_status_array",
        parameters = @org.hibernate.annotations.Parameter(
            name = "sql_array_type",
            value = "seat_status"
        )
    )
    @Column(
        name = "seat_grid",
        columnDefinition = "seat_status[][]"
    )
    private SeatStatus[][] seatGrid;
 
    //Getters and setters omitted for brevity

    public SeatStatus getSeatStatus(int row, char letter) {
        return seatGrid[row - 1][letter - 65];
    }
}

因此,您需要声明适当的Hibernate类型以供使用。对于枚举,您需要使用EnumArrayType

@TypeDef(
    name = "seat_status_array",
    typeClass = EnumArrayType.class
)
< p > @Type 注释允许您向 Hibernate Type 传递参数,例如 SQL 数组类:

@Type(
    type = "seat_status_array",
    parameters = @org.hibernate.annotations.Parameter(
        name = "sql_array_type",
        value = "seat_status"
    )
)

测试时间

现在,当您持久化以下文章(Post)实体时:

entityManager.persist(
    new Plane()
        .setId(1L)
        .setName("ATR-42")
        .setSeatGrid(
            new SeatStatus[][] {
                {
                    SeatStatus.BLOCKED, SeatStatus.BLOCKED,
                    SeatStatus.BLOCKED, SeatStatus.BLOCKED
                },
                {
                    SeatStatus.UNRESERVED, SeatStatus.UNRESERVED,
                    SeatStatus.RESERVED, SeatStatus.UNRESERVED
                },
                {
                    SeatStatus.RESERVED, SeatStatus.RESERVED,
                    SeatStatus.RESERVED, SeatStatus.RESERVED
                }
            }
        )
);

Hibernate 将会生成正确的 SQL INSERT 语句:
INSERT INTO plane (
    name,
    seat_grid,
    id
)
VALUES (
    'ATR-42',
    {
        {"BLOCKED", "BLOCKED", "BLOCKED", "BLOCKED"},
        {"UNRESERVED", "UNRESERVED", "RESERVED", "UNRESERVED"},
        {"RESERVED", "RESERVED", "RESERVED", "RESERVED"}
    },
    1
)

当获取实体时,一切都按预期工作:

Plane plane = entityManager.find(Plane.class, 1L);

assertEquals("ATR-42", plane.getName());
assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'A'));
assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'B'));
assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'C'));
assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'D'));
assertEquals(SeatStatus.UNRESERVED, plane.getSeatStatus(2, 'A'));
assertEquals(SeatStatus.UNRESERVED, plane.getSeatStatus(2, 'B'));
assertEquals(SeatStatus.RESERVED, plane.getSeatStatus(2, 'C'));
assertEquals(SeatStatus.UNRESERVED, plane.getSeatStatus(2, 'D'));

3
为一个10年前的问题添加一个新的(更好的)答案!这样做会获得徽章吗? :-) - GeertPt
3
那是“持之以恒”徽章。 - Vlad Mihalcea
在我看来,这似乎是一个打字错误... 你是不是想说PersistentBag? - jens
这只适用于Postgress作为数据库?对于不支持(多维)对象数组的其他数据库,有什么好的解决方案呢? - Robert van der Spek
1
很少有数据库支持数组。对于这些情况,解决方法是使用JSON代替。在JSON对象中,您可以模拟嵌套数组。 - Vlad Mihalcea

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接