diff --git a/bee.persistent.test/datasource.test/src/main/kotlin/com/beeproduced/datasource/test/manytomany/ManyToManyEntities.kt b/bee.persistent.test/datasource.test/src/main/kotlin/com/beeproduced/datasource/test/manytomany/ManyToManyEntities.kt new file mode 100644 index 0000000..d0cd094 --- /dev/null +++ b/bee.persistent.test/datasource.test/src/main/kotlin/com/beeproduced/datasource/test/manytomany/ManyToManyEntities.kt @@ -0,0 +1,64 @@ +package com.beeproduced.datasource.test.manytomany + +import com.beeproduced.bee.persistent.blaze.BeeBlazeRepository +import com.beeproduced.bee.persistent.blaze.annotations.BeeRepository +import jakarta.persistence.* +import java.io.Serializable + +/** + * + * + * @author Kacper Urbaniec + * @version 2024-01-15 + */ + +@Entity +@Table(name = "foos") +data class Foo( + @Id + @GeneratedValue + val id: Long = -1, + @ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.DETACH]) + @JoinTable( + name = "foo_bar_relations", + joinColumns = [JoinColumn(name = "foo")], + inverseJoinColumns = [JoinColumn(name = "bar")] + ) + // Use sets instead of lists to allow fetching multiple collections eagerly at once + // to omit "MultipleBagFetchException - cannot simultaneously fetch multiple bags" + // https://stackoverflow.com/a/4335514/12347616 + val bars: Set? = null +) + + +@BeeRepository +interface FooRepository : BeeBlazeRepository + +@Entity +@Table(name = "bars") +data class Bar( + @Id + @GeneratedValue + val id: Long = -1, + @ManyToMany(mappedBy="bars", fetch = FetchType.LAZY, cascade = [CascadeType.DETACH]) + val foos: Set? = null +) + +@BeeRepository +interface BarRepository : BeeBlazeRepository + +@Embeddable +data class FooBarId( + val foo: Long = -1, + val bar: Long = -1, +) : Serializable + +@Entity +@Table(name = "foo_bar_relations") +data class FooBarRelation( + @EmbeddedId + val id: FooBarId +) + +@BeeRepository +interface FooBarRepository : BeeBlazeRepository \ No newline at end of file diff --git a/bee.persistent.test/src/test/kotlin/com/beeproduced/bee/persistent/test/base/ManyToManyTest.kt b/bee.persistent.test/src/test/kotlin/com/beeproduced/bee/persistent/test/base/ManyToManyTest.kt new file mode 100644 index 0000000..8cecce2 --- /dev/null +++ b/bee.persistent.test/src/test/kotlin/com/beeproduced/bee/persistent/test/base/ManyToManyTest.kt @@ -0,0 +1,163 @@ +package com.beeproduced.bee.persistent.test.base + +import com.beeproduced.datasource.a.FooBar +import com.beeproduced.datasource.test.dsl.BarDSL +import com.beeproduced.datasource.test.dsl.FooDSL +import com.beeproduced.datasource.test.manytomany.* +import jakarta.persistence.EntityManager +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.TestPropertySource +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.support.TransactionTemplate +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * + * + * @author Kacper Urbaniec + * @version 2024-01-15 + */ +@ExtendWith(SpringExtension::class) +@SpringBootTest(classes = [BaseTestConfig::class]) +@TestPropertySource("classpath:application.properties") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ManyToManyTest( + @Qualifier("testEM") + val em: EntityManager, + @Qualifier("testTM") + transactionManager: PlatformTransactionManager, + @Autowired + val fooRepo: FooRepository, + @Autowired + val barRepo: BarRepository, + @Autowired + val fooBarRepo: FooBarRepository +) { + private val transaction = TransactionTemplate(transactionManager) + + @BeforeAll + fun beforeAll() = clear() + + @AfterEach + fun afterEach() = clear() + + fun clear() = transaction.executeWithoutResult { + fooBarRepo.cbf.delete(em, FooBarRelation::class.java) + fooRepo.cbf.delete(em, Foo::class.java).executeUpdate() + barRepo.cbf.delete(em, Bar::class.java).executeUpdate() + } + + @Test + fun `add relation`() { + var barId: Long = -1 + var foo1Id: Long = -1 + var foo2Id: Long = -1 + var foo3Id: Long = -1 + + transaction.executeWithoutResult { + val bar = barRepo.persist(Bar()) + barId = bar.id + val foo1 = fooRepo.persist(Foo()) + foo1Id = foo1.id + val foo2 = fooRepo.persist(Foo()) + foo2Id = foo2.id + val foo3 = fooRepo.persist(Foo()) + foo3Id = foo3.id + } + + transaction.executeWithoutResult { + val bars = barRepo.select() + assertEquals(1, bars.count()) + val foos = fooRepo.select() + assertEquals(3, foos.count()) + } + + transaction.executeWithoutResult { + fooBarRepo.persist(FooBarRelation(FooBarId(foo1Id, barId))) + } + + val barSelection = BarDSL.select { this.foos { this.bars { this.foos { } } } } + val fooSelection = FooDSL.select { this.bars { this.foos { this.bars { } } } } + transaction.executeWithoutResult { + val bar = barRepo.select(barSelection) { + where(BarDSL.id.eq(barId)) + }.firstOrNull() + assertNotNull(bar) + assertBar(bar, setOf(barId), setOf(foo1Id), 3) + val foo1 = fooRepo.select(fooSelection) { + where(FooDSL.id.eq(foo1Id)) + }.firstOrNull() + assertNotNull(foo1) + assertFoo(foo1, setOf(foo1Id), setOf(barId), 3) + val foo2 = fooRepo.select(fooSelection) { + where(FooDSL.id.eq(foo2Id)) + }.firstOrNull() + assertNotNull(foo2) + assertFoo(foo2, setOf(foo2Id), emptySet(), 3) + } + + transaction.executeWithoutResult { + fooBarRepo.persist(FooBarRelation(FooBarId(foo2Id, barId))) + } + + transaction.executeWithoutResult { + val bar = barRepo.select(barSelection) { + where(BarDSL.id.eq(barId)) + }.firstOrNull() + assertNotNull(bar) + assertBar(bar, setOf(barId), setOf(foo1Id, foo2Id), 3) + val foo1 = fooRepo.select(fooSelection) { + where(FooDSL.id.eq(foo1Id)) + }.firstOrNull() + assertNotNull(foo1) + assertFoo(foo1, setOf(foo1Id, foo2Id), setOf(barId), 3) + val foo2 = fooRepo.select(fooSelection) { + where(FooDSL.id.eq(foo2Id)) + }.firstOrNull() + assertNotNull(foo2) + assertFoo(foo2, setOf(foo2Id, foo1Id), setOf(barId), 3) + } + } + + + private fun assertBar( + bar: Bar, barIds: Set, fooIds: Set, depth: Int + ) { + assertTrue { barIds.contains(bar.id) } + val foos = bar.foos + if (depth == 0) { + assertTrue { foos.isNullOrEmpty() } + return + } + + assertNotNull(foos) + for (foo in foos) + assertFoo(foo, fooIds, barIds, depth - 1) + } + + private fun assertFoo( + foo: Foo, fooIds: Set, barIds: Set, depth: Int + ) { + assertTrue { fooIds.contains(foo.id) } + val bars = foo.bars + if (depth == 0) { + assertTrue { bars.isNullOrEmpty() } + return + } + + assertNotNull(bars) + for (bar in bars) + assertBar(bar, barIds, fooIds, depth - 1) + } + +} \ No newline at end of file