Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4204836
Fix @OneToOne composite @JoinColumn handling for @EmbeddedId in JDBC
radovanradic May 19, 2026
2bcf345
Trigger build
radovanradic May 19, 2026
38fe350
Fix @OneToOne composite @JoinColumn handling for @EmbeddedId in JDBC
radovanradic May 19, 2026
71e57ad
Fix @OneToOne composite @JoinColumn handling for @EmbeddedId in JDBC
radovanradic May 19, 2026
a3cd680
Fix shared-key @OneToOne insert/DDL column reuse for embedded and seq…
radovanradic May 20, 2026
9a214c0
Fix checkstyle errors.
radovanradic May 20, 2026
fbd1382
Fix fetch-join projection for one-to-one @JoinColumns with embedded I…
radovanradic May 20, 2026
cac741d
Narrow shared-identity column reuse to explicit join columns and add …
radovanradic May 20, 2026
dae48de
Omit generated shared-identity columns from insert statements.
radovanradic May 20, 2026
1ed0d74
Add H2 regression coverage for shared-id one-to-one update
radovanradic May 20, 2026
96d3c4c
Narrow shared-identity update skip to identity columns
radovanradic May 20, 2026
6f2bbab
Consolidate shared-identity SQL mapping checks
radovanradic May 20, 2026
549bf26
Add guard tests for explicit non-shared join column naming
radovanradic May 20, 2026
8a1470a
Remove branch build trigger
radovanradic May 20, 2026
5aa0ac1
Add some comments in the code
radovanradic May 20, 2026
cbfe607
Simplify test
radovanradic May 20, 2026
d53e8af
Merge branch '5.0.x' into issue-3415
radovanradic May 20, 2026
df99f48
Merge remote-tracking branch 'origin/5.0.x' into issue-3415
radovanradic May 26, 2026
8f3bfff
Address Sonar issue
radovanradic May 27, 2026
d0f0910
Merge remote-tracking branch 'origin/5.1.x' into issue-3415
radovanradic May 29, 2026
56c4885
Change class to record, as suggested by Sonar check.
radovanradic Jun 15, 2026
df4347b
Merge branch '5.1.x' into issue-3415
radovanradic Jun 15, 2026
a738f5f
Minor improvements.
radovanradic Jun 15, 2026
7054ee3
Address review comments.
radovanradic Jun 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.micronaut.data.jdbc.h2.one2one

import io.micronaut.data.jdbc.h2.H2DBProperties
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification

import java.sql.Connection
import java.util.UUID

@MicronautTest
@H2DBProperties(packages = "io.micronaut.data.jdbc.h2.one2one", schemaGenerate = "NONE")
class OneToOneEmbeddedIdJoinColumnRecordSpec extends Specification {

@Inject
RecordAssetRepository recordAssetRepository

@Inject
Connection connection

void setup() {
try (def s = connection.createStatement()) {
s.execute('''
DROP TABLE IF EXISTS record_asset;
DROP TABLE IF EXISTS record_assetmetadata;

CREATE TABLE record_asset (
container_id UUID NOT NULL,
asset_id INTEGER NOT NULL,
title VARCHAR(255),
PRIMARY KEY (container_id, asset_id)
);

CREATE TABLE record_assetmetadata (
container_id UUID NOT NULL,
asset_id INTEGER NOT NULL,
author VARCHAR(255),
PRIMARY KEY (container_id, asset_id)
);

INSERT INTO record_assetmetadata (container_id, asset_id, author) VALUES ('6f8d3ed4-46e3-4656-9e89-cd61ac1e4cf8', 1, 'chris');
INSERT INTO record_asset (container_id, asset_id, title) VALUES ('6f8d3ed4-46e3-4656-9e89-cd61ac1e4cf8', 1, 'Llama Llama');
''')
}
}

void 'fetch join owning one-to-one with composite join columns and embedded id records'() {
given:
def id = new RecordAssetId(UUID.fromString('6f8d3ed4-46e3-4656-9e89-cd61ac1e4cf8'), 1)

when:
def asset = recordAssetRepository.findById(id).orElse(null)

then:
asset != null
asset.metadata() != null
asset.metadata().author() == 'chris'
asset.metadata().id() == id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package io.micronaut.data.jdbc.h2.one2one

import io.micronaut.data.annotation.Embeddable
import io.micronaut.data.annotation.EmbeddedId
import io.micronaut.data.annotation.Join
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.data.annotation.MappedProperty
import io.micronaut.data.annotation.Relation
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.jdbc.h2.H2DBProperties
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.CrudRepository
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import jakarta.persistence.JoinColumn
import spock.lang.Shared
import spock.lang.Specification

import java.sql.Connection
import java.util.UUID

@MicronautTest
@H2DBProperties(packages = "io.micronaut.data.jdbc.h2.one2one", schemaGenerate = "NONE")
class OneToOneEmbeddedIdJoinColumnSpec extends Specification {

@Shared
@Inject
AssetRepository assetRepository

@Shared
@Inject
Connection connection

void setup() {
try (def s = connection.createStatement()) {
s.execute('''
DROP TABLE IF EXISTS asset;
DROP TABLE IF EXISTS assetmetadata;

CREATE TABLE asset (
container_id UUID NOT NULL,
asset_id INTEGER NOT NULL,
title VARCHAR(255),
PRIMARY KEY (container_id, asset_id)
);

CREATE TABLE assetmetadata (
container_id UUID NOT NULL,
asset_id INTEGER NOT NULL,
author VARCHAR(255),
PRIMARY KEY (container_id, asset_id)
);
''')
}
}

void 'save owning one-to-one with composite join columns and embedded id'() {
given:
def id = new AssetId(containerId: UUID.randomUUID(), assetId: 1)

when:
assetRepository.save(new Asset(id: id, title: 'title'))
def saved = assetRepository.findById(id).orElse(null)

then:
saved != null
saved.id.containerId == id.containerId
saved.id.assetId == id.assetId
saved.title == 'title'
}

void 'update owning one-to-one does not write shared identity from relation path'() {
given:
def id = new AssetId(containerId: UUID.randomUUID(), assetId: 1)
assetRepository.save(new Asset(id: id, title: 'title'))

when:
assetRepository.update(new Asset(id: id, title: 'updated', metadata: null))
def updated = assetRepository.findById(id).orElse(null)

then:
updated != null
updated.id.containerId == id.containerId
updated.id.assetId == id.assetId
updated.title == 'updated'
}

void 'fetch join owning one-to-one with composite join columns and embedded id'() {
given:
def id = new AssetId(containerId: UUID.fromString('6f8d3ed4-46e3-4656-9e89-cd61ac1e4cf8'), assetId: 1)
try (def s = connection.createStatement()) {
s.execute("""
INSERT INTO assetmetadata (container_id, asset_id, author) VALUES ('${id.containerId}', ${id.assetId}, 'chris');
INSERT INTO asset (container_id, asset_id, title) VALUES ('${id.containerId}', ${id.assetId}, 'Llama Llama');
""")
}

when:
def asset = assetRepository.findById(id).orElse(null)

then:
asset != null
asset.metadata != null
asset.metadata.author == 'chris'
}
}

@JdbcRepository(dialect = Dialect.H2)
interface AssetRepository extends CrudRepository<Asset, AssetId> {

@Join(value = "metadata", type = Join.Type.LEFT_FETCH)
@Override
Optional<Asset> findById(AssetId id)
}

@Embeddable
class AssetId {

@MappedProperty("container_id")
UUID containerId

@MappedProperty("asset_id")
Integer assetId
}

@MappedEntity("asset")
class Asset {

@EmbeddedId
AssetId id

String title

@Relation(value = Relation.Kind.ONE_TO_ONE, cascade = Relation.Cascade.NONE)
@JoinColumn(name = "container_id", referencedColumnName = "container_id")
@JoinColumn(name = "asset_id", referencedColumnName = "asset_id")
AssetMetadata metadata
}

@MappedEntity("assetmetadata")
class AssetMetadata {

@EmbeddedId
AssetId id

String author
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package io.micronaut.data.jdbc.postgres.one2one

import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.data.annotation.Relation
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.jdbc.postgres.PostgresTestPropertyProvider
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.runtime.config.SchemaGenerate
import io.micronaut.data.repository.CrudRepository
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import jakarta.persistence.JoinColumn
import spock.lang.Specification

import java.sql.Connection

@MicronautTest
class PostgresOneToOneSharedSequenceIdentitySpec extends Specification implements PostgresTestPropertyProvider {

@Inject
Connection connection

@Inject
SharedSequenceAssetRepository sharedSequenceAssetRepository

@Override
List<String> packages() {
return List.of("io.micronaut.data.jdbc.postgres.one2one")
}

@Override
SchemaGenerate schemaGenerate() {
return SchemaGenerate.NONE
}

void setup() {
connection.prepareStatement('''
DROP TABLE IF EXISTS sequence_assetmetadata;
DROP TABLE IF EXISTS sequence_asset;
DROP SEQUENCE IF EXISTS sequence_asset_seq;

CREATE SEQUENCE sequence_asset_seq START WITH 1 INCREMENT BY 1;

CREATE TABLE sequence_asset (
id BIGINT NOT NULL PRIMARY KEY,
title VARCHAR(255)
);

CREATE TABLE sequence_assetmetadata (
id BIGINT NOT NULL PRIMARY KEY,
author VARCHAR(255)
);
''').withCloseable { it.executeUpdate() }
}

void 'save shared-key one-to-one with sequence identity reuses the physical id column'() {
when:
def saved = sharedSequenceAssetRepository.save(new SharedSequenceAsset(title: 'title'))

then:
saved.id == 1L
sharedSequenceAssetRepository.findById(saved.id).orElse(null)?.title == 'title'
}
}

@JdbcRepository(dialect = Dialect.POSTGRES)
interface SharedSequenceAssetRepository extends CrudRepository<SharedSequenceAsset, Long> {
}

@MappedEntity("sequence_asset")
class SharedSequenceAsset {

@Id
@GeneratedValue(value = GeneratedValue.Type.SEQUENCE, ref = "sequence_asset_seq")
Long id

String title

@Relation(value = Relation.Kind.ONE_TO_ONE, cascade = Relation.Cascade.NONE)
@JoinColumn(name = "id", referencedColumnName = "id")
SharedSequenceAssetMetadata metadata
}

@MappedEntity("sequence_assetmetadata")
class SharedSequenceAssetMetadata {

@Id
Long id

String author
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.micronaut.data.jdbc.h2.one2one;

import io.micronaut.data.annotation.EmbeddedId;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.Relation;
import jakarta.persistence.JoinColumn;

@MappedEntity("record_asset")
public record RecordAsset(
@EmbeddedId RecordAssetId id,
String title,
@Relation(value = Relation.Kind.ONE_TO_ONE, cascade = Relation.Cascade.NONE)
@JoinColumn(name = "container_id", referencedColumnName = "container_id")
@JoinColumn(name = "asset_id", referencedColumnName = "asset_id")
RecordAssetMetadata metadata
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.micronaut.data.jdbc.h2.one2one;

import io.micronaut.data.annotation.Embeddable;
import io.micronaut.data.annotation.MappedProperty;

import java.util.UUID;

@Embeddable
public record RecordAssetId(
@MappedProperty("container_id") UUID containerId,
@MappedProperty("asset_id") Integer assetId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.micronaut.data.jdbc.h2.one2one;

import io.micronaut.data.annotation.EmbeddedId;
import io.micronaut.data.annotation.MappedEntity;

@MappedEntity("record_assetmetadata")
public record RecordAssetMetadata(
@EmbeddedId RecordAssetId id,
String author
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.micronaut.data.jdbc.h2.one2one;

import io.micronaut.data.annotation.Join;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
import io.micronaut.data.repository.jpa.JpaSpecificationExecutor;

import java.util.Optional;

@JdbcRepository(dialect = Dialect.H2)
public interface RecordAssetRepository extends CrudRepository<RecordAsset, RecordAssetId>, JpaSpecificationExecutor<RecordAsset> {

@Join("metadata")
Optional<RecordAsset> findById(RecordAssetId id);
}
Loading
Loading