Sping Boot with Hibernate:多对多关系-entity1_has_entity2表为空-缺少什么?

qaxu7uf2  于 2023-08-06  发布在  其他
关注(0)|答案(1)|浏览(138)

我有两个实体。
bandproduct之间有一种 * 多对多的关系 *,因为一个产品可以有许多乐队(比如一个采样器,上面有许多乐队),而一个乐队可以有许多产品(比如衬衫,cd等)。
当我运行我的测试(见下文)时,它保存了2个实体,但它没有保存band_has_product表中2个实体之间的关系。
据我所知,我不需要额外的存储库/实体来将关系保存到band_has_product表中,因为所有的注解和休眠,它应该自动完成。
是这样吗?
我很确定,我错过了一些东西,我看了很多教程,但我找不到我的问题。期待一些帮助。谢啦,谢啦
下面是代码示例:
product实体是这样创建的:

@Entity
data class Product(

    @Id
    @Setter(AccessLevel.NONE)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Schema(hidden = true)
    val id: Int? = null,

    var name: @NotEmpty(message = "name must not be empty") String? = null,

    ) {
    @JsonBackReference(value ="bands")
    @ManyToMany( fetch = FetchType.EAGER,
        mappedBy = "products",
    )
    var bands: MutableSet<Band> = mutableSetOf()

}

字符串
而带实体看起来像这样:

@Entity
data class Band(

    @Id
    @Setter(AccessLevel.NONE)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Schema(hidden = true)
    val id: Int? = null,

    var name: @NotEmpty(message = "name must not be empty") String? = null,

    var description: String? = null,
){
    @JsonBackReference(value ="products")
    @ManyToMany(
        fetch = FetchType.EAGER,
        cascade = [CascadeType.ALL])
    @JoinTable(
        name = "band_has_product",
        joinColumns = [JoinColumn(name = "band_id")],
        inverseJoinColumns = [JoinColumn(name = "product_id")]
    )
    var products: MutableSet<Product> = mutableSetOf()
}


乐队是这样创建的:

create table band
(
    id serial PRIMARY KEY not null,
    name varchar(100) not null,
    description text

);


产品是这样创建的:

create table product
(
    id serial PRIMARY KEY not null,
    name varchar(100) not null
);


我写了一个测试来测试这个。波段和产品已保存,但自动生成的band_has_product表为空

@Test
fun `creating Product should be saved in the DB`() {
    var bandSaved1 = bandRepository!!.save(BandUtils.createBandWithoutLinks("band1"))

    productRepository!!.save(ProductUtils.createProductWithBand(
        bandSaved1,"product1"))

    val bands = (bandRepository!!.findAll() as Collection<Band>)
    val products = (productRepository!!.findAll() as Collection<Product>)

    bands.size shouldBe 1  // TRUE
    products.size shouldBe 1  // TRUE

    // not true, as the band_has_product table is empty
    val product = products.first().bands shouldContain bandSaved1


Test helper函数如下所示:

fun createBandWithoutLinks(name: String = BAND_NAME_WITHOUT_LINK): Band {
        val band = Band()
        band.name = name
        band.description = "merch em all"
        return band
  }

fun createProductWithBand(band: Band,
                          name: String = PRODUCT_NAME): Product {
    var product = Product(
        name = name,
    )
    product.bands.add(band)
    return product
}


仓库是普通的CrudRepos:

@Repository
interface BandRepository : CrudRepository<Band?, Int?> {}

@Repository
interface ProductRepository : CrudRepository<Product?, Int?> {}


我使用标准的保存功能。
MainApp看起来像这样:

@EnableJpaRepositories
@SpringBootApplication
class MainApp

fun main(args: Array<String>) {
    runApplication<MainApp>(*args)
}

xwbd5t1u

xwbd5t1u1#

Steven Haines中读取"Java persistence with JPA and Hibernate, Part 2: Many-to-many relationships",您可以看到,在使用JPA和Hibernate的多对多关系中,您需要正确地管理关系的双方。
例如,在测试助手函数createProductWithBand中:

fun createProductWithBand(band: Band, name: String = PRODUCT_NAME): Product {
    var product = Product(
        name = name,
    )
    product.bands.add(band)
    return product
}

字符串
在此函数中,将创建一个Product,并将一个Band添加到其bands集合中。然而,相反的情况并没有发生; Product没有被添加到Bandproducts集合中。
你的测试函数:

@Test
fun `creating Product should be saved in the DB`() {
    var bandSaved1 = bandRepository!!.save(BandUtils.createBandWithoutLinks("band1"))

    productRepository!!.save(ProductUtils.createProductWithBand(bandSaved1,"product1"))

    // ...
}


在这里,保存了一个Band,然后用该Band创建了一个Product并保存。但是,代码中没有任何地方将Product添加到Bandproducts集合中。
这意味着只设置了关系的一侧(即Product知道Band,但Band不知道Product)。
在像这样的双向关系中,必须更新关系的两端。JPA提供者(在本例中为Hibernate)需要了解双方的关系,以便正确地更新连接表(band_has_product)。

fun createProductWithBand(band: Band, name: String = PRODUCT_NAME): Product {
    var product = Product(
        name = name,
    )
    product.bands.add(band)

    // Also set the reverse relationship
    band.products.add(product)

    return product
}


通过这样做,当你保存Product时,Hibernate将知道它与Band相关联,同样,当你保存Band时,Hibernate将知道它与Product相关联。这将确保正确填充连接表band_has_product
从OP的answer中,我可以添加在Kotlin中,plus函数返回一个新列表,其中包括正在添加的元素,但它不会修改原始列表。因此,当您再次调用band.products.size时,它仍然是0,因为原始列表没有被修改。
要正确更新Band实体的products集合,可以将plus的结果赋回Bandproducts属性。这将使用添加的新产品更新products集合。
根据你的方法,这将是:

@Test
fun `creating Product should be saved in the DB`() {
    var bandSaved = bandRepository!!.save(BandUtils.createBandWithoutLinks("band1"))

    var product = ProductUtils.createProductWithBand(bandSaved, "product1")
    bandSaved.products = bandSaved.products.plus(product)  // Update the products collection

    val productSaved = productRepository!!.save(product)

    val bands = (bandRepository!!.findAll() as Collection<Band>)
    val products = (productRepository!!.findAll() as Collection<Product>)

    bands.size shouldBe 1  // TRUE
    products.size shouldBe 1  // TRUE

    val product = products.first().bands shouldContain bandSaved
}


修改后的helper函数:

fun createProductWithBand(band: Band, name: String = PRODUCT_NAME): Product {
    var product = Product(name = name)
    product.bands = product.bands.plus(band)  // Update the bands collection
    return product
}


通过将plus的结果赋值回productsbands集合,您正在使用新实体更新集合,并且更改将正确地持久化。
由于代码现在直接修改集合,因此在这个特定场景中不再需要显式地使用@Transactional
但是,请注意,在更复杂的场景或真实世界的应用程序中,可能仍然需要使用事务来确保数据一致性和操作的原子性。
Nathan在评论中添加:
对于写操作,将乘积添加到bank是唯一重要的操作。带有mappedBy=""@ManyToMany注解在加载时使用,但在持久化或更新时不使用。
在测试中,如果您希望正确加载mappedBy="",您可能需要手动刷新、逐出或刷新,以便正确加载mappedBy=""(如果您只设置了一侧)。
至少,这就是hibernate 3.x、4x和5.x的工作方式。没有太多的经验与6。但我认为这在6年里没有改变。
确实,在这里,Band实体是关系的拥有方(因为它不使用mappedBy属性),而Product实体是相反方(因为它使用mappedBy属性)。
在测试时,如果您在关系的反向端(在本例中为Product)进行了更改,并且希望它立即反映在会话缓存中,则可能需要手动将会话刷新到数据库并刷新实体。这是因为Hibernate可能在事务提交之前不会将更改同步到数据库。

@Test
@Transactional
fun `creating Product should be saved in the DB`() {
    var band = BandUtils.createBandWithoutLinks("band1")
    var product = ProductUtils.createProductWithBand(band, "product1")

    // Save the band (owning side of the relationship)
    band = bandRepository!!.save(band)

    // Save the product (inverse side of the relationship)
    product = productRepository!!.save(product)

    // Flush changes immediately to the database
    entityManager.flush()

    // Refresh the product entity to reflect the changes
    entityManager.refresh(product)

    val bands = (bandRepository!!.findAll() as Collection<Band>)
    val products = (productRepository!!.findAll() as Collection<Product>)

    bands.size shouldBe 1  // TRUE
    products.size shouldBe 1  // TRUE

    // Now it should be true as both sides of the relationship are set
    val product = products.first().bands shouldContain band
}


警告:entityManager应该自动连接到测试类中,这样才能正常工作。
这种做法通常在测试中更常见。
在生产代码中,通常不需要在同一事务中强制刷新或刷新,因为更改将在事务结束时自动同步。
您还需要确保这些操作是在事务中执行的。这是因为,在JPA中,持久化上下文通常在事务结束时刷新到数据库。
另见“JPA/Hibernate Persistence Context”from Baeldung

@Test
@Transactional  // Ensure the entire method is run within a single transaction
fun `creating Product should be saved in the DB`() {
    var band = BandUtils.createBandWithoutLinks("band1")
    var product = ProductUtils.createProductWithBand(band, "product1")

    // Save both the band and the product
    band = bandRepository!!.save(band)
    product = productRepository!!.save(product)

    val bands = (bandRepository!!.findAll() as Collection<Band>)
    val products = (productRepository!!.findAll() as Collection<Product>)

    bands.size shouldBe 1  // TRUE
    products.size shouldBe 1  // TRUE

    // Now it should be true as both sides of the relationship are set
    val product = products.first().bands shouldContain band
}


这应该确保当您保存Product时,相应的波段也会被保存,反之亦然,并且关系存储在band_has_product表中。
它使用事务范围的持久化上下文,这是JPA中使用Java EE或Spring环境时的默认持久化上下文类型。
@Transactional注解确保整个方法在单个事务中运行。这意味着在方法开始时创建一个持久性上下文,并且在此持久性上下文中跟踪对实体所做的任何更改。
当方法完成时,事务被提交或回滚,并且持久性上下文被关闭。此时,对事务中的托管实体所做的任何更改都将刷新到数据库。

这种类型的持久性上下文适合于所提供的场景,在这种场景中,您希望在单个原子操作中保存实体及其关系,以确保数据一致性。

相关问题