Add inheritance module: SINGLE_TABLE vs JOINED vs TABLE_PER_CLASS on Hibernate 7.4.13.Final
Same payment hierarchy mapped three ways plus @MappedSuperclass, run with plain Hibernate on H2. Tests assert and write the generated DDL, the INSERT/SELECT SQL, row/insert/FK/nullability counts and the three failures (NOT NULL on a SINGLE_TABLE subclass column, IDENTITY with TABLE_PER_CLASS, querying a @MappedSuperclass); Bench writes indicative polymorphic-query timings. No docs folder: depth lives in the post.
This commit is contained in:
@@ -334,3 +334,12 @@ hibernate-demo/
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
MIT — see [LICENSE](LICENSE).
|
MIT — see [LICENSE](LICENSE).
|
||||||
|
|
||||||
|
## Standalone modules
|
||||||
|
|
||||||
|
Modules with their own `pom.xml` that run plain Hibernate without the Spring Boot parent above:
|
||||||
|
|
||||||
|
| Module | What it covers |
|
||||||
|
|---|---|
|
||||||
|
| [`h8-preview`](h8-preview/) | Hibernate ORM 8 beta against Hibernate 7: what breaks |
|
||||||
|
| [`inheritance`](inheritance/) | `SINGLE_TABLE` vs `JOINED` vs `TABLE_PER_CLASS` vs `@MappedSuperclass`: generated DDL, SQL, failures and timings (Hibernate `7.4.13.Final`) |
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# inheritance
|
||||||
|
|
||||||
|
Companion module for the ankurm.com article on **Hibernate 7 inheritance mapping: `SINGLE_TABLE` vs `JOINED` vs
|
||||||
|
`TABLE_PER_CLASS`**. One payment hierarchy (a root with card, bank-transfer and wallet subclasses, plus an order that
|
||||||
|
points at the root) mapped three ways, run against plain Hibernate (no Spring) and in-memory H2, with every statement
|
||||||
|
Hibernate generates captured in `output/`.
|
||||||
|
|
||||||
|
## Versions
|
||||||
|
|
||||||
|
| Component | Version | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Hibernate ORM | `7.4.13.Final` | latest 7.x GA on Maven Central on 2026-10-24 (`8.0.0.Beta3` is also listed; it is not GA) |
|
||||||
|
| Jakarta Persistence | `3.2.0` | |
|
||||||
|
| H2 | `2.4.240` | in memory, `OPTIMIZE_REUSE_RESULTS=0` (see `Db.java`) |
|
||||||
|
| JUnit Jupiter / AssertJ | `6.0.3` / `3.27.7` | |
|
||||||
|
| Java | `25` (Temurin 25.0.5+8) | |
|
||||||
|
|
||||||
|
This module has its own `pom.xml` and does not use the Spring Boot parent of the main `hibernate-demo` project, so its
|
||||||
|
Hibernate patch version differs from the one that project pins.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/run-all.sh # JUnit tests, then the timing run; regenerates everything in output/
|
||||||
|
./scripts/bench.sh # only the timing run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
| Path | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `src/main/java/com/ankurm/inheritance/single` | `Payment` hierarchy, `SINGLE_TABLE`, discriminator column, `@Check` constraint |
|
||||||
|
| `src/main/java/com/ankurm/inheritance/joined` | the same hierarchy, `JOINED` |
|
||||||
|
| `src/main/java/com/ankurm/inheritance/tpc` | the same hierarchy, `TABLE_PER_CLASS` (sequence ids, abstract root) |
|
||||||
|
| `src/main/java/com/ankurm/inheritance/mapped` | `@MappedSuperclass` `AuditedBase` with two unrelated entities |
|
||||||
|
| `src/test/java/.../Strategies.java` | the three hierarchies side by side |
|
||||||
|
| `src/test/java/.../DdlTest.java` | writes `output/01-*` |
|
||||||
|
| `src/test/java/.../SqlTest.java` | writes `output/02-*` |
|
||||||
|
| `src/test/java/.../ContractsTest.java` | writes `output/03-contracts.txt`; also holds the two deliberately wrong mappings (`Sn*`, `TpcIdentity*`) |
|
||||||
|
| `src/test/java/.../Bench.java` | writes `output/04-polymorphic-timings.txt` |
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
| File | What it shows |
|
||||||
|
|---|---|
|
||||||
|
| `output/01-ddl-single-table.txt`, `01-ddl-joined.txt`, `01-ddl-table-per-class.txt`, `01-ddl-mapped-superclass.txt` | the schema Hibernate generates |
|
||||||
|
| `output/02-sql-single-table.txt`, `02-sql-joined.txt`, `02-sql-table-per-class.txt` | INSERTs and SELECTs for the same four operations |
|
||||||
|
| `output/03-contracts.txt` | row counts, INSERT counts, nullability, foreign keys, and the three failures |
|
||||||
|
| `output/04-polymorphic-timings.txt` | indicative timings on 150,000 rows (not asserted; drift between runs) |
|
||||||
|
|
||||||
|
Everything in `01`-`03` is produced by tests that assert the numbers they print.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
-- DDL generated by Hibernate 7.4.13.Final for H2, strategy JOINED
|
||||||
|
-- tables created: 5
|
||||||
|
|
||||||
|
|
||||||
|
create sequence jn_order_seq start with 1 increment by 50;
|
||||||
|
|
||||||
|
create sequence jn_payment_seq start with 1 increment by 50;
|
||||||
|
|
||||||
|
create table jn_bank_transfer_payment (
|
||||||
|
payment_id bigint not null,
|
||||||
|
bank_code varchar(11),
|
||||||
|
iban varchar(34) not null,
|
||||||
|
primary key (payment_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table jn_card_payment (
|
||||||
|
card_last4 varchar(4) not null,
|
||||||
|
payment_id bigint not null,
|
||||||
|
network varchar(16),
|
||||||
|
primary key (payment_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table jn_order (
|
||||||
|
id bigint not null,
|
||||||
|
payment_id bigint not null,
|
||||||
|
reference varchar(20) not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table jn_payment (
|
||||||
|
amount numeric(12,2) not null,
|
||||||
|
currency varchar(3) not null,
|
||||||
|
id bigint not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table jn_wallet_payment (
|
||||||
|
payment_id bigint not null,
|
||||||
|
provider varchar(24) not null,
|
||||||
|
primary key (payment_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table if exists jn_bank_transfer_payment
|
||||||
|
add constraint FK7mx6hp714jwtc7cbp5oboi9b5
|
||||||
|
foreign key (payment_id)
|
||||||
|
references jn_payment;
|
||||||
|
|
||||||
|
alter table if exists jn_card_payment
|
||||||
|
add constraint FK2pe4xdfpn9xujq97a21nj00nr
|
||||||
|
foreign key (payment_id)
|
||||||
|
references jn_payment;
|
||||||
|
|
||||||
|
alter table if exists jn_order
|
||||||
|
add constraint FKschxw26qcnlfcgeyirv55ttkl
|
||||||
|
foreign key (payment_id)
|
||||||
|
references jn_payment;
|
||||||
|
|
||||||
|
alter table if exists jn_wallet_payment
|
||||||
|
add constraint FK9cf00xcddbti8l83t1s5ptu3h
|
||||||
|
foreign key (payment_id)
|
||||||
|
references jn_payment;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- DDL generated by Hibernate 7.4.13.Final for H2, @MappedSuperclass AuditedBase + Invoice + Receipt
|
||||||
|
|
||||||
|
|
||||||
|
create sequence mp_invoice_SEQ start with 1 increment by 50;
|
||||||
|
|
||||||
|
create sequence mp_receipt_SEQ start with 1 increment by 50;
|
||||||
|
|
||||||
|
create table mp_invoice (
|
||||||
|
version integer not null,
|
||||||
|
created_at timestamp(6) with time zone not null,
|
||||||
|
id bigint not null,
|
||||||
|
number varchar(20) not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table mp_receipt (
|
||||||
|
version integer not null,
|
||||||
|
created_at timestamp(6) with time zone not null,
|
||||||
|
id bigint not null,
|
||||||
|
note varchar(60),
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- DDL generated by Hibernate 7.4.13.Final for H2, strategy SINGLE_TABLE
|
||||||
|
-- tables created: 2
|
||||||
|
|
||||||
|
|
||||||
|
create sequence st_order_seq start with 1 increment by 50;
|
||||||
|
|
||||||
|
create sequence st_payment_seq start with 1 increment by 50;
|
||||||
|
|
||||||
|
create table st_order (
|
||||||
|
id bigint not null,
|
||||||
|
payment_id bigint not null,
|
||||||
|
reference varchar(20) not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table st_payment (
|
||||||
|
amount numeric(12,2) not null,
|
||||||
|
card_last4 varchar(4),
|
||||||
|
currency varchar(3) not null,
|
||||||
|
id bigint not null,
|
||||||
|
bank_code varchar(11),
|
||||||
|
payment_type varchar(12) not null check ((payment_type in ('CARD','BANK','WALLET'))),
|
||||||
|
network varchar(16),
|
||||||
|
provider varchar(24),
|
||||||
|
iban varchar(34),
|
||||||
|
primary key (id),
|
||||||
|
constraint st_payment_subclass_columns check ((payment_type <> 'CARD' or card_last4 is not null) and (payment_type <> 'BANK' or iban is not null) and (payment_type <> 'WALLET' or provider is not null))
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table if exists st_order
|
||||||
|
add constraint FKdi3y406uyk3cm6po29lmjirt5
|
||||||
|
foreign key (payment_id)
|
||||||
|
references st_payment;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- DDL generated by Hibernate 7.4.13.Final for H2, strategy TABLE_PER_CLASS
|
||||||
|
-- tables created: 4
|
||||||
|
|
||||||
|
|
||||||
|
create sequence tp_order_seq start with 1 increment by 50;
|
||||||
|
|
||||||
|
create sequence tp_payment_seq start with 1 increment by 50;
|
||||||
|
|
||||||
|
create table tp_bank_transfer_payment (
|
||||||
|
amount numeric(12,2) not null,
|
||||||
|
currency varchar(3) not null,
|
||||||
|
id bigint not null,
|
||||||
|
bank_code varchar(11),
|
||||||
|
iban varchar(34) not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table tp_card_payment (
|
||||||
|
amount numeric(12,2) not null,
|
||||||
|
card_last4 varchar(4) not null,
|
||||||
|
currency varchar(3) not null,
|
||||||
|
id bigint not null,
|
||||||
|
network varchar(16),
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table tp_order (
|
||||||
|
id bigint not null,
|
||||||
|
payment_id bigint not null,
|
||||||
|
reference varchar(20) not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table tp_wallet_payment (
|
||||||
|
amount numeric(12,2) not null,
|
||||||
|
currency varchar(3) not null,
|
||||||
|
id bigint not null,
|
||||||
|
provider varchar(24) not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- strategy JOINED, Hibernate 7.4.13.Final
|
||||||
|
|
||||||
|
== persist 3 payments and 1 order: 7 INSERT statements
|
||||||
|
insert into jn_payment (amount,currency,id) values (?,?,?);
|
||||||
|
insert into jn_card_payment (card_last4,network,payment_id) values (?,?,?);
|
||||||
|
insert into jn_payment (amount,currency,id) values (?,?,?);
|
||||||
|
insert into jn_bank_transfer_payment (bank_code,iban,payment_id) values (?,?,?);
|
||||||
|
insert into jn_payment (amount,currency,id) values (?,?,?);
|
||||||
|
insert into jn_wallet_payment (provider,payment_id) values (?,?);
|
||||||
|
insert into jn_order (payment_id,reference,id) values (?,?,?);
|
||||||
|
|
||||||
|
== select p from JnPayment p (polymorphic, returns 3 rows)
|
||||||
|
select p1_0.id,case when p1_2.payment_id is not null then 1 when p1_1.payment_id is not null then 2 when p1_3.payment_id is not null then 3 end,p1_0.amount,p1_0.currency,p1_1.bank_code,p1_1.iban,p1_2.card_last4,p1_2.network,p1_3.provider
|
||||||
|
from jn_payment p1_0
|
||||||
|
left join jn_bank_transfer_payment p1_1 on p1_0.id=p1_1.payment_id
|
||||||
|
left join jn_card_payment p1_2 on p1_0.id=p1_2.payment_id
|
||||||
|
left join jn_wallet_payment p1_3 on p1_0.id=p1_3.payment_id
|
||||||
|
order by p1_0.id;
|
||||||
|
|
||||||
|
== from JnCardPayment (one subclass, returns 1 row)
|
||||||
|
select cp1_0.payment_id,cp1_1.amount,cp1_1.currency,cp1_0.card_last4,cp1_0.network
|
||||||
|
from jn_card_payment cp1_0
|
||||||
|
join jn_payment cp1_1 on cp1_0.payment_id=cp1_1.id;
|
||||||
|
|
||||||
|
== select o from JnOrder o join fetch o.payment (association to the root)
|
||||||
|
select po1_0.id,po1_0.payment_id,p1_0.id,case when p1_2.payment_id is not null then 1 when p1_1.payment_id is not null then 2 when p1_3.payment_id is not null then 3 end,p1_0.amount,p1_0.currency,p1_1.bank_code,p1_1.iban,p1_2.card_last4,p1_2.network,p1_3.provider,po1_0.reference
|
||||||
|
from jn_order po1_0
|
||||||
|
join jn_payment p1_0 on p1_0.id=po1_0.payment_id
|
||||||
|
left join jn_bank_transfer_payment p1_1 on p1_0.id=p1_1.payment_id
|
||||||
|
left join jn_card_payment p1_2 on p1_0.id=p1_2.payment_id
|
||||||
|
left join jn_wallet_payment p1_3 on p1_0.id=p1_3.payment_id;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- strategy SINGLE_TABLE, Hibernate 7.4.13.Final
|
||||||
|
|
||||||
|
== persist 3 payments and 1 order: 4 INSERT statements
|
||||||
|
insert into st_payment (amount,currency,card_last4,network,payment_type,id) values (?,?,?,?,'CARD',?);
|
||||||
|
insert into st_payment (amount,currency,bank_code,iban,payment_type,id) values (?,?,?,?,'BANK',?);
|
||||||
|
insert into st_payment (amount,currency,provider,payment_type,id) values (?,?,?,'WALLET',?);
|
||||||
|
insert into st_order (payment_id,reference,id) values (?,?,?);
|
||||||
|
|
||||||
|
== select p from StPayment p (polymorphic, returns 3 rows)
|
||||||
|
select p1_0.id,p1_0.payment_type,p1_0.amount,p1_0.currency,p1_0.bank_code,p1_0.iban,p1_0.card_last4,p1_0.network,p1_0.provider
|
||||||
|
from st_payment p1_0
|
||||||
|
order by p1_0.id;
|
||||||
|
|
||||||
|
== from StCardPayment (one subclass, returns 1 row)
|
||||||
|
select cp1_0.id,cp1_0.amount,cp1_0.currency,cp1_0.card_last4,cp1_0.network
|
||||||
|
from st_payment cp1_0
|
||||||
|
where cp1_0.payment_type='CARD';
|
||||||
|
|
||||||
|
== select o from StOrder o join fetch o.payment (association to the root)
|
||||||
|
select po1_0.id,po1_0.payment_id,p1_0.id,p1_0.payment_type,p1_0.amount,p1_0.currency,p1_0.bank_code,p1_0.iban,p1_0.card_last4,p1_0.network,p1_0.provider,po1_0.reference
|
||||||
|
from st_order po1_0
|
||||||
|
join st_payment p1_0 on p1_0.id=po1_0.payment_id;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- strategy TABLE_PER_CLASS, Hibernate 7.4.13.Final
|
||||||
|
|
||||||
|
== persist 3 payments and 1 order: 4 INSERT statements
|
||||||
|
insert into tp_card_payment (amount,currency,card_last4,network,id) values (?,?,?,?,?);
|
||||||
|
insert into tp_bank_transfer_payment (amount,currency,bank_code,iban,id) values (?,?,?,?,?);
|
||||||
|
insert into tp_wallet_payment (amount,currency,provider,id) values (?,?,?,?);
|
||||||
|
insert into tp_order (payment_id,reference,id) values (?,?,?);
|
||||||
|
|
||||||
|
== select p from TpPayment p (polymorphic, returns 3 rows)
|
||||||
|
select p1_0.id,p1_0.clazz_,p1_0.amount,p1_0.currency,p1_0.bank_code,p1_0.iban,p1_0.card_last4,p1_0.network,p1_0.provider
|
||||||
|
from (select amount, card_last4, currency, id, network, null as bank_code, null as iban, null as provider, 1 as clazz_
|
||||||
|
from tp_card_payment
|
||||||
|
union all
|
||||||
|
select amount, null as card_last4, currency, id, null as network, bank_code, iban, null as provider, 2 as clazz_
|
||||||
|
from tp_bank_transfer_payment
|
||||||
|
union all
|
||||||
|
select amount, null as card_last4, currency, id, null as network, null as bank_code, null as iban, provider, 3 as clazz_
|
||||||
|
from tp_wallet_payment) p1_0
|
||||||
|
order by p1_0.id;
|
||||||
|
|
||||||
|
== from TpCardPayment (one subclass, returns 1 row)
|
||||||
|
select cp1_0.id,cp1_0.amount,cp1_0.currency,cp1_0.card_last4,cp1_0.network
|
||||||
|
from tp_card_payment cp1_0;
|
||||||
|
|
||||||
|
== select o from TpOrder o join fetch o.payment (association to the root)
|
||||||
|
select po1_0.id,po1_0.payment_id,p1_0.id,p1_0.clazz_,p1_0.amount,p1_0.currency,p1_0.bank_code,p1_0.iban,p1_0.card_last4,p1_0.network,p1_0.provider,po1_0.reference
|
||||||
|
from tp_order po1_0
|
||||||
|
join (select amount, card_last4, currency, id, network, null as bank_code, null as iban, null as provider, 1 as clazz_
|
||||||
|
from tp_card_payment
|
||||||
|
union all
|
||||||
|
select amount, null as card_last4, currency, id, null as network, bank_code, iban, null as provider, 2 as clazz_
|
||||||
|
from tp_bank_transfer_payment
|
||||||
|
union all
|
||||||
|
select amount, null as card_last4, currency, id, null as network, null as bank_code, null as iban, provider, 3 as clazz_
|
||||||
|
from tp_wallet_payment) p1_0 on p1_0.id=po1_0.payment_id;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
Hibernate 7.4.13.Final, H2 in memory
|
||||||
|
|
||||||
|
== SINGLE_TABLE
|
||||||
|
INSERT statements to save 3 payments + 1 order: 4
|
||||||
|
tables and row counts:
|
||||||
|
st_order = 1
|
||||||
|
st_payment = 3
|
||||||
|
IS_NULLABLE for card_last4 in st_payment: YES
|
||||||
|
foreign keys on st_order: 1
|
||||||
|
|
||||||
|
discriminator values stored: [BANK, CARD, WALLET]
|
||||||
|
raw INSERT of a CARD row with no card_last4: Check constraint violation: "ST_PAYMENT_SUBCLASS_COLUMNS: "; SQL statement:
|
||||||
|
insert into st_payment (id, amount, currency, payment_type) values (900, 1.00, 'EUR', 'CARD') [23513-240]
|
||||||
|
|
||||||
|
== JOINED
|
||||||
|
INSERT statements to save 3 payments + 1 order: 7
|
||||||
|
tables and row counts:
|
||||||
|
jn_bank_transfer_payment = 1
|
||||||
|
jn_card_payment = 1
|
||||||
|
jn_order = 1
|
||||||
|
jn_payment = 3
|
||||||
|
jn_wallet_payment = 1
|
||||||
|
IS_NULLABLE for card_last4 in jn_card_payment: NO
|
||||||
|
foreign keys on jn_order: 1
|
||||||
|
|
||||||
|
== TABLE_PER_CLASS
|
||||||
|
INSERT statements to save 3 payments + 1 order: 4
|
||||||
|
tables and row counts:
|
||||||
|
tp_bank_transfer_payment = 1
|
||||||
|
tp_card_payment = 1
|
||||||
|
tp_order = 1
|
||||||
|
tp_wallet_payment = 1
|
||||||
|
IS_NULLABLE for card_last4 in tp_card_payment: NO
|
||||||
|
foreign keys on tp_order: 0
|
||||||
|
|
||||||
|
== SINGLE_TABLE with nullable = false on subclass columns
|
||||||
|
IS_NULLABLE: [A_VALUE | NO, B_VALUE | NO, ID | NO, KIND | NO]
|
||||||
|
persist(new SnA()): org.hibernate.exception.ConstraintViolationException: could not execute statement [NULL not allowed for column "B_VALUE"; SQL statement:
|
||||||
|
insert into sn_root (a_value,kind,id) values (?,'A',?) [23502-240]] [insert into sn_root (a_value,kind,id) values (?,'A',?)]
|
||||||
|
|
||||||
|
== TABLE_PER_CLASS with GenerationType.IDENTITY
|
||||||
|
org.hibernate.MappingException: Cannot use identity column key generation with <union-subclass> mapping for: com.ankurm.inheritance.TpcIdentityCard
|
||||||
|
|
||||||
|
== querying a @MappedSuperclass
|
||||||
|
tables: [mp_invoice, mp_receipt]
|
||||||
|
org.hibernate.query.sqm.UnknownEntityException: Could not resolve root entity 'AuditedBase'
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
INDICATIVE ONLY - timings drift between runs and machines; nothing here is asserted.
|
||||||
|
Hibernate 7.4.13.Final, H2 in memory, JDK 25.0.5+8-LTS, 2 vCPU
|
||||||
|
150000 payments (50000 card, 50000 bank transfer, 50000 wallet), read through a StatelessSession, median of 15 runs after 5 warm-ups
|
||||||
|
|
||||||
|
strategy query rows median ms
|
||||||
|
single-table all payments (polymorphic) 150000 247.2
|
||||||
|
single-table one subclass (CardPayment) 50000 82.4
|
||||||
|
single-table count where amount > 500 (root col) 1 18.4
|
||||||
|
single-table first 50 by id (polymorphic) 50 0.3
|
||||||
|
|
||||||
|
joined all payments (polymorphic) 150000 401.5
|
||||||
|
joined one subclass (CardPayment) 50000 107.1
|
||||||
|
joined count where amount > 500 (root col) 1 21.6
|
||||||
|
joined first 50 by id (polymorphic) 50 0.3
|
||||||
|
|
||||||
|
table-per-class all payments (polymorphic) 150000 422.0
|
||||||
|
table-per-class one subclass (CardPayment) 50000 82.1
|
||||||
|
table-per-class count where amount > 500 (root col) 1 56.0
|
||||||
|
table-per-class first 50 by id (polymorphic) 50 165.8
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>inheritance</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>inheritance</name>
|
||||||
|
<description>Hibernate 7 inheritance mapping: SINGLE_TABLE vs JOINED vs TABLE_PER_CLASS, run with plain Hibernate (no Spring)</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.release>25</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<hibernate.version>7.4.13.Final</hibernate.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate.orm</groupId>
|
||||||
|
<artifactId>hibernate-core</artifactId>
|
||||||
|
<version>${hibernate.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.persistence</groupId>
|
||||||
|
<artifactId>jakarta.persistence-api</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<version>2.4.240</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>6.0.3</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.27.7</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Writes output/04-polymorphic-timings.txt. Timings are indicative: they drift between runs and machines.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
mvn -B -q test-compile dependency:build-classpath -Dmdep.outputFile=target/cp.txt -Dmdep.includeScope=test
|
||||||
|
java -cp "target/classes:target/test-classes:$(cat target/cp.txt)" com.ankurm.inheritance.Bench
|
||||||
|
cat output/04-polymorphic-timings.txt
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates every file under output/. Needs JDK 25 and Maven 3.9 with access to Maven Central.
|
||||||
|
# 01-ddl-*, 02-sql-*, 03-contracts : the JUnit tests (they assert the numbers they print)
|
||||||
|
# 04-polymorphic-timings : scripts/bench.sh (indicative timings, not asserted)
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
mvn -B clean test
|
||||||
|
./scripts/bench.sh
|
||||||
|
ls -1 output
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ankurm.inheritance.joined;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity(name = "JnBankTransferPayment")
|
||||||
|
@Table(name = "jn_bank_transfer_payment")
|
||||||
|
@PrimaryKeyJoinColumn(name = "payment_id")
|
||||||
|
public class BankTransferPayment extends Payment {
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 34)
|
||||||
|
private String iban;
|
||||||
|
|
||||||
|
@Column(name = "bank_code", length = 11)
|
||||||
|
private String bankCode;
|
||||||
|
|
||||||
|
protected BankTransferPayment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public BankTransferPayment(BigDecimal amount, String currency, String iban, String bankCode) {
|
||||||
|
super(amount, currency);
|
||||||
|
this.iban = iban;
|
||||||
|
this.bankCode = bankCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ankurm.inheritance.joined;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity(name = "JnCardPayment")
|
||||||
|
@Table(name = "jn_card_payment")
|
||||||
|
@PrimaryKeyJoinColumn(name = "payment_id")
|
||||||
|
public class CardPayment extends Payment {
|
||||||
|
|
||||||
|
@Column(name = "card_last4", nullable = false, length = 4)
|
||||||
|
private String cardLast4;
|
||||||
|
|
||||||
|
@Column(length = 16)
|
||||||
|
private String network;
|
||||||
|
|
||||||
|
protected CardPayment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public CardPayment(BigDecimal amount, String currency, String cardLast4, String network) {
|
||||||
|
super(amount, currency);
|
||||||
|
this.cardLast4 = cardLast4;
|
||||||
|
this.network = network;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.ankurm.inheritance.joined;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/** JOINED: one table per class; a subclass row is the root row plus a row in the subclass table, joined on the primary key. */
|
||||||
|
@Entity(name = "JnPayment")
|
||||||
|
@Table(name = "jn_payment")
|
||||||
|
@Inheritance(strategy = InheritanceType.JOINED)
|
||||||
|
public abstract class Payment {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "jn_payment_seq")
|
||||||
|
@SequenceGenerator(name = "jn_payment_seq", sequenceName = "jn_payment_seq", allocationSize = 50)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, precision = 12, scale = 2)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 3)
|
||||||
|
private String currency;
|
||||||
|
|
||||||
|
protected Payment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Payment(BigDecimal amount, String currency) {
|
||||||
|
this.amount = amount;
|
||||||
|
this.currency = currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public BigDecimal getAmount() { return amount; }
|
||||||
|
public String getCurrency() { return currency; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ankurm.inheritance.joined;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
/** An order that points at the hierarchy root; what that foreign key looks like depends on the strategy. */
|
||||||
|
@Entity(name = "JnOrder")
|
||||||
|
@Table(name = "jn_order")
|
||||||
|
public class PaymentOrder {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "jn_order_seq")
|
||||||
|
@SequenceGenerator(name = "jn_order_seq", sequenceName = "jn_order_seq", allocationSize = 50)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private String reference;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "payment_id")
|
||||||
|
private Payment payment;
|
||||||
|
|
||||||
|
protected PaymentOrder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentOrder(String reference, Payment payment) {
|
||||||
|
this.reference = reference;
|
||||||
|
this.payment = payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public String getReference() { return reference; }
|
||||||
|
public Payment getPayment() { return payment; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ankurm.inheritance.joined;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity(name = "JnWalletPayment")
|
||||||
|
@Table(name = "jn_wallet_payment")
|
||||||
|
@PrimaryKeyJoinColumn(name = "payment_id")
|
||||||
|
public class WalletPayment extends Payment {
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 24)
|
||||||
|
private String provider;
|
||||||
|
|
||||||
|
protected WalletPayment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletPayment(BigDecimal amount, String currency, String provider) {
|
||||||
|
super(amount, currency);
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ankurm.inheritance.mapped;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @MappedSuperclass: shared columns and mappings, but not an entity. It has no table, cannot be
|
||||||
|
* queried, and cannot be the target of an association.
|
||||||
|
*/
|
||||||
|
@MappedSuperclass
|
||||||
|
public abstract class AuditedBase {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private int version;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private Instant createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
void stamp() {
|
||||||
|
this.createdAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public int getVersion() { return version; }
|
||||||
|
public Instant getCreatedAt() { return createdAt; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.inheritance.mapped;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity(name = "MpInvoice")
|
||||||
|
@Table(name = "mp_invoice")
|
||||||
|
public class Invoice extends AuditedBase {
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private String number;
|
||||||
|
|
||||||
|
protected Invoice() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Invoice(String number) {
|
||||||
|
this.number = number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNumber() { return number; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.inheritance.mapped;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity(name = "MpReceipt")
|
||||||
|
@Table(name = "mp_receipt")
|
||||||
|
public class Receipt extends AuditedBase {
|
||||||
|
|
||||||
|
@Column(length = 60)
|
||||||
|
private String note;
|
||||||
|
|
||||||
|
protected Receipt() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Receipt(String note) {
|
||||||
|
this.note = note;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNote() { return note; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ankurm.inheritance.single;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity(name = "StBankTransferPayment")
|
||||||
|
@DiscriminatorValue("BANK")
|
||||||
|
public class BankTransferPayment extends Payment {
|
||||||
|
|
||||||
|
@Column(length = 34)
|
||||||
|
private String iban;
|
||||||
|
|
||||||
|
@Column(name = "bank_code", length = 11)
|
||||||
|
private String bankCode;
|
||||||
|
|
||||||
|
protected BankTransferPayment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public BankTransferPayment(BigDecimal amount, String currency, String iban, String bankCode) {
|
||||||
|
super(amount, currency);
|
||||||
|
this.iban = iban;
|
||||||
|
this.bankCode = bankCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ankurm.inheritance.single;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity(name = "StCardPayment")
|
||||||
|
@DiscriminatorValue("CARD")
|
||||||
|
public class CardPayment extends Payment {
|
||||||
|
|
||||||
|
@Column(name = "card_last4", length = 4)
|
||||||
|
private String cardLast4;
|
||||||
|
|
||||||
|
@Column(length = 16)
|
||||||
|
private String network;
|
||||||
|
|
||||||
|
protected CardPayment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public CardPayment(BigDecimal amount, String currency, String cardLast4, String network) {
|
||||||
|
super(amount, currency);
|
||||||
|
this.cardLast4 = cardLast4;
|
||||||
|
this.network = network;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ankurm.inheritance.single;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.Check;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/** SINGLE_TABLE: one table for the whole hierarchy, a discriminator column says which subclass a row is. */
|
||||||
|
@Entity(name = "StPayment")
|
||||||
|
@Table(name = "st_payment")
|
||||||
|
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
|
||||||
|
@DiscriminatorColumn(name = "payment_type", length = 12)
|
||||||
|
@Check(name = "st_payment_subclass_columns", constraints = "(payment_type <> 'CARD' or card_last4 is not null) and (payment_type <> 'BANK' or iban is not null) and (payment_type <> 'WALLET' or provider is not null)")
|
||||||
|
public abstract class Payment {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "st_payment_seq")
|
||||||
|
@SequenceGenerator(name = "st_payment_seq", sequenceName = "st_payment_seq", allocationSize = 50)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, precision = 12, scale = 2)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 3)
|
||||||
|
private String currency;
|
||||||
|
|
||||||
|
protected Payment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Payment(BigDecimal amount, String currency) {
|
||||||
|
this.amount = amount;
|
||||||
|
this.currency = currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public BigDecimal getAmount() { return amount; }
|
||||||
|
public String getCurrency() { return currency; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ankurm.inheritance.single;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
/** An order that points at the hierarchy root; what that foreign key looks like depends on the strategy. */
|
||||||
|
@Entity(name = "StOrder")
|
||||||
|
@Table(name = "st_order")
|
||||||
|
public class PaymentOrder {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "st_order_seq")
|
||||||
|
@SequenceGenerator(name = "st_order_seq", sequenceName = "st_order_seq", allocationSize = 50)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private String reference;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "payment_id")
|
||||||
|
private Payment payment;
|
||||||
|
|
||||||
|
protected PaymentOrder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentOrder(String reference, Payment payment) {
|
||||||
|
this.reference = reference;
|
||||||
|
this.payment = payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public String getReference() { return reference; }
|
||||||
|
public Payment getPayment() { return payment; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.inheritance.single;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity(name = "StWalletPayment")
|
||||||
|
@DiscriminatorValue("WALLET")
|
||||||
|
public class WalletPayment extends Payment {
|
||||||
|
|
||||||
|
@Column(length = 24)
|
||||||
|
private String provider;
|
||||||
|
|
||||||
|
protected WalletPayment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletPayment(BigDecimal amount, String currency, String provider) {
|
||||||
|
super(amount, currency);
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ankurm.inheritance.tpc;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity(name = "TpBankTransferPayment")
|
||||||
|
@Table(name = "tp_bank_transfer_payment")
|
||||||
|
public class BankTransferPayment extends Payment {
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 34)
|
||||||
|
private String iban;
|
||||||
|
|
||||||
|
@Column(name = "bank_code", length = 11)
|
||||||
|
private String bankCode;
|
||||||
|
|
||||||
|
protected BankTransferPayment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public BankTransferPayment(BigDecimal amount, String currency, String iban, String bankCode) {
|
||||||
|
super(amount, currency);
|
||||||
|
this.iban = iban;
|
||||||
|
this.bankCode = bankCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ankurm.inheritance.tpc;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity(name = "TpCardPayment")
|
||||||
|
@Table(name = "tp_card_payment")
|
||||||
|
public class CardPayment extends Payment {
|
||||||
|
|
||||||
|
@Column(name = "card_last4", nullable = false, length = 4)
|
||||||
|
private String cardLast4;
|
||||||
|
|
||||||
|
@Column(length = 16)
|
||||||
|
private String network;
|
||||||
|
|
||||||
|
protected CardPayment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public CardPayment(BigDecimal amount, String currency, String cardLast4, String network) {
|
||||||
|
super(amount, currency);
|
||||||
|
this.cardLast4 = cardLast4;
|
||||||
|
this.network = network;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ankurm.inheritance.tpc;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/** TABLE_PER_CLASS: one table per concrete class, each carrying every inherited column; the abstract root has no table. */
|
||||||
|
@Entity(name = "TpPayment")
|
||||||
|
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
|
||||||
|
public abstract class Payment {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tp_payment_seq")
|
||||||
|
@SequenceGenerator(name = "tp_payment_seq", sequenceName = "tp_payment_seq", allocationSize = 50)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, precision = 12, scale = 2)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 3)
|
||||||
|
private String currency;
|
||||||
|
|
||||||
|
protected Payment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Payment(BigDecimal amount, String currency) {
|
||||||
|
this.amount = amount;
|
||||||
|
this.currency = currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public BigDecimal getAmount() { return amount; }
|
||||||
|
public String getCurrency() { return currency; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ankurm.inheritance.tpc;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
/** An order that points at the hierarchy root; what that foreign key looks like depends on the strategy. */
|
||||||
|
@Entity(name = "TpOrder")
|
||||||
|
@Table(name = "tp_order")
|
||||||
|
public class PaymentOrder {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tp_order_seq")
|
||||||
|
@SequenceGenerator(name = "tp_order_seq", sequenceName = "tp_order_seq", allocationSize = 50)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private String reference;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "payment_id")
|
||||||
|
private Payment payment;
|
||||||
|
|
||||||
|
protected PaymentOrder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentOrder(String reference, Payment payment) {
|
||||||
|
this.reference = reference;
|
||||||
|
this.payment = payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public String getReference() { return reference; }
|
||||||
|
public Payment getPayment() { return payment; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.inheritance.tpc;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity(name = "TpWalletPayment")
|
||||||
|
@Table(name = "tp_wallet_payment")
|
||||||
|
public class WalletPayment extends Payment {
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 24)
|
||||||
|
private String provider;
|
||||||
|
|
||||||
|
protected WalletPayment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletPayment(BigDecimal amount, String currency, String provider) {
|
||||||
|
super(amount, currency);
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import org.hibernate.StatelessSession;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not a test: timings drift between runs and machines, so nothing here is asserted.
|
||||||
|
* Writes output/04-polymorphic-timings.txt. Run it with scripts/bench.sh.
|
||||||
|
*/
|
||||||
|
public final class Bench {
|
||||||
|
|
||||||
|
static final int PER_TYPE = 50_000;
|
||||||
|
static final int WARMUP = 5;
|
||||||
|
static final int RUNS = 15;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
try (Transcript t = new Transcript("04-polymorphic-timings")) {
|
||||||
|
t.line("INDICATIVE ONLY - timings drift between runs and machines; nothing here is asserted.");
|
||||||
|
t.line("Hibernate " + org.hibernate.Version.getVersionString() + ", H2 in memory, JDK " + Runtime.version()
|
||||||
|
+ ", " + Runtime.getRuntime().availableProcessors() + " vCPU");
|
||||||
|
t.line(3 * PER_TYPE + " payments (" + PER_TYPE + " card, " + PER_TYPE + " bank transfer, " + PER_TYPE
|
||||||
|
+ " wallet), read through a StatelessSession, median of " + RUNS + " runs after " + WARMUP + " warm-ups");
|
||||||
|
t.blank();
|
||||||
|
t.line(String.format("%-16s %-44s %8s %10s", "strategy", "query", "rows", "median ms"));
|
||||||
|
for (Strategies s : Strategies.values()) {
|
||||||
|
try (Db db = new Db(s.classes())) {
|
||||||
|
load(db, s);
|
||||||
|
db.clear();
|
||||||
|
String p = s.prefix;
|
||||||
|
query(t, db, s, "all payments (polymorphic)", "select p from " + p + "Payment p", null);
|
||||||
|
query(t, db, s, "one subclass (CardPayment)", "from " + p + "CardPayment", null);
|
||||||
|
query(t, db, s, "count where amount > 500 (root col)", "select count(p) from " + p + "Payment p where p.amount > 500", null);
|
||||||
|
query(t, db, s, "first 50 by id (polymorphic)", "select p from " + p + "Payment p order by p.id", 50);
|
||||||
|
}
|
||||||
|
t.blank();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void query(Transcript t, Db db, Strategies s, String label, String hql, Integer max) {
|
||||||
|
List<Double> times = new ArrayList<>();
|
||||||
|
int rows = 0;
|
||||||
|
try (StatelessSession session = db.sessionFactory.openStatelessSession()) {
|
||||||
|
for (int i = 0; i < WARMUP + RUNS; i++) {
|
||||||
|
long start = System.nanoTime();
|
||||||
|
var q = session.createSelectionQuery(hql, Object.class);
|
||||||
|
if (max != null) q.setMaxResults(max);
|
||||||
|
rows = q.getResultList().size();
|
||||||
|
double ms = (System.nanoTime() - start) / 1e6;
|
||||||
|
if (i >= WARMUP) times.add(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Collections.sort(times);
|
||||||
|
t.line(String.format("%-16s %-44s %8d %10.1f", s.label, label, rows, times.get(times.size() / 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void load(Db db, Strategies s) {
|
||||||
|
try (StatelessSession session = db.sessionFactory.openStatelessSession()) {
|
||||||
|
session.getTransaction().begin();
|
||||||
|
for (int i = 0; i < PER_TYPE; i++) {
|
||||||
|
BigDecimal amount = BigDecimal.valueOf(1 + (i % 1000));
|
||||||
|
session.insert(make(s, "card", amount, i));
|
||||||
|
session.insert(make(s, "bank", amount, i));
|
||||||
|
session.insert(make(s, "wallet", amount, i));
|
||||||
|
}
|
||||||
|
session.getTransaction().commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object make(Strategies s, String type, BigDecimal amount, int i) {
|
||||||
|
Supplier<Object> f = switch (s) {
|
||||||
|
case SINGLE_TABLE -> switch (type) {
|
||||||
|
case "card" -> () -> new com.ankurm.inheritance.single.CardPayment(amount, "EUR", "4242", "VISA");
|
||||||
|
case "bank" -> () -> new com.ankurm.inheritance.single.BankTransferPayment(amount, "EUR", "DE89370400440532013000", "COBADEFF");
|
||||||
|
default -> () -> new com.ankurm.inheritance.single.WalletPayment(amount, "EUR", "PAYPAL");
|
||||||
|
};
|
||||||
|
case JOINED -> switch (type) {
|
||||||
|
case "card" -> () -> new com.ankurm.inheritance.joined.CardPayment(amount, "EUR", "4242", "VISA");
|
||||||
|
case "bank" -> () -> new com.ankurm.inheritance.joined.BankTransferPayment(amount, "EUR", "DE89370400440532013000", "COBADEFF");
|
||||||
|
default -> () -> new com.ankurm.inheritance.joined.WalletPayment(amount, "EUR", "PAYPAL");
|
||||||
|
};
|
||||||
|
case TABLE_PER_CLASS -> switch (type) {
|
||||||
|
case "card" -> () -> new com.ankurm.inheritance.tpc.CardPayment(amount, "EUR", "4242", "VISA");
|
||||||
|
case "bank" -> () -> new com.ankurm.inheritance.tpc.BankTransferPayment(amount, "EUR", "DE89370400440532013000", "COBADEFF");
|
||||||
|
default -> () -> new com.ankurm.inheritance.tpc.WalletPayment(amount, "EUR", "PAYPAL");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return f.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import com.ankurm.inheritance.mapped.AuditedBase;
|
||||||
|
import com.ankurm.inheritance.mapped.Invoice;
|
||||||
|
import com.ankurm.inheritance.mapped.Receipt;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
/** Writes output/03-contracts.txt: row counts, statement counts, nullability, foreign keys and the two famous failures. */
|
||||||
|
class ContractsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contracts() {
|
||||||
|
try (Transcript t = new Transcript("03-contracts")) {
|
||||||
|
t.line("Hibernate " + org.hibernate.Version.getVersionString() + ", H2 in memory");
|
||||||
|
t.blank();
|
||||||
|
|
||||||
|
for (Strategies s : Strategies.values()) {
|
||||||
|
try (Db db = new Db(s.classes())) {
|
||||||
|
db.sessionFactory.inTransaction(session -> s.sample().forEach(session::persist));
|
||||||
|
long inserts = db.sql.stream().filter(q -> q.startsWith("insert")).count();
|
||||||
|
t.line("== " + s.name());
|
||||||
|
t.line("INSERT statements to save 3 payments + 1 order: " + inserts);
|
||||||
|
t.line("tables and row counts:");
|
||||||
|
for (String table : db.jdbc("select table_name from information_schema.tables where table_schema = 'PUBLIC' and table_name like '" + s.tablePrefix.toUpperCase() + "\\_%' escape '\\' order by table_name")) {
|
||||||
|
t.line(" " + table.toLowerCase() + " = " + db.jdbc("select count(*) from " + table).get(0));
|
||||||
|
}
|
||||||
|
String card = s.tablePrefix + (s == Strategies.SINGLE_TABLE ? "_payment" : "_card_payment");
|
||||||
|
t.line("IS_NULLABLE for card_last4 in " + card + ": "
|
||||||
|
+ db.jdbc("select is_nullable from information_schema.columns where table_name = '" + card.toUpperCase() + "' and column_name = 'CARD_LAST4'").get(0));
|
||||||
|
t.line("foreign keys on " + s.tablePrefix + "_order: "
|
||||||
|
+ db.jdbc("select count(*) from information_schema.table_constraints where table_name = '" + (s.tablePrefix + "_order").toUpperCase() + "' and constraint_type = 'FOREIGN KEY'").get(0));
|
||||||
|
t.blank();
|
||||||
|
|
||||||
|
switch (s) {
|
||||||
|
case SINGLE_TABLE -> {
|
||||||
|
assertThat(inserts).isEqualTo(4);
|
||||||
|
assertThat(db.jdbc("select payment_type from st_payment order by payment_type")).containsExactly("BANK", "CARD", "WALLET");
|
||||||
|
assertThat(db.jdbc("select is_nullable from information_schema.columns where table_name = 'ST_PAYMENT' and column_name = 'CARD_LAST4'")).containsExactly("YES");
|
||||||
|
assertThat(db.jdbc("select count(*) from information_schema.table_constraints where table_name = 'ST_ORDER' and constraint_type = 'FOREIGN KEY'")).containsExactly("1");
|
||||||
|
t.line("discriminator values stored: " + db.jdbc("select payment_type from st_payment order by payment_type"));
|
||||||
|
String bad = db.tryExec("insert into st_payment (id, amount, currency, payment_type) values (900, 1.00, 'EUR', 'CARD')");
|
||||||
|
t.line("raw INSERT of a CARD row with no card_last4: " + bad);
|
||||||
|
assertThat(bad).contains("st_payment_subclass_columns".toUpperCase());
|
||||||
|
t.blank();
|
||||||
|
}
|
||||||
|
case JOINED -> {
|
||||||
|
assertThat(inserts).isEqualTo(7);
|
||||||
|
assertThat(db.jdbc("select is_nullable from information_schema.columns where table_name = 'JN_CARD_PAYMENT' and column_name = 'CARD_LAST4'")).containsExactly("NO");
|
||||||
|
assertThat(db.jdbc("select count(*) from information_schema.table_constraints where table_name = 'JN_ORDER' and constraint_type = 'FOREIGN KEY'")).containsExactly("1");
|
||||||
|
}
|
||||||
|
case TABLE_PER_CLASS -> {
|
||||||
|
assertThat(inserts).isEqualTo(4);
|
||||||
|
assertThat(db.jdbc("select is_nullable from information_schema.columns where table_name = 'TP_CARD_PAYMENT' and column_name = 'CARD_LAST4'")).containsExactly("NO");
|
||||||
|
assertThat(db.jdbc("select count(*) from information_schema.table_constraints where table_name = 'TP_ORDER' and constraint_type = 'FOREIGN KEY'")).containsExactly("0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SINGLE_TABLE and nullable=false on a subclass column
|
||||||
|
t.line("== SINGLE_TABLE with nullable = false on subclass columns");
|
||||||
|
try (Db db = new Db(SnRoot.class, SnA.class, SnB.class)) {
|
||||||
|
t.line("IS_NULLABLE: " + db.jdbc("select column_name, is_nullable from information_schema.columns where table_name = 'SN_ROOT' order by column_name"));
|
||||||
|
assertThatThrownBy(() -> db.sessionFactory.inTransaction(s -> s.persist(new SnA())))
|
||||||
|
.satisfies(e -> t.line("persist(new SnA()): " + e.getClass().getName() + ": " + e.getMessage()));
|
||||||
|
}
|
||||||
|
t.blank();
|
||||||
|
|
||||||
|
// TABLE_PER_CLASS cannot use IDENTITY: a row in any of the three tables must not reuse an id from another
|
||||||
|
t.line("== TABLE_PER_CLASS with GenerationType.IDENTITY");
|
||||||
|
assertThatThrownBy(() -> new Db(TpcIdentityPayment.class, TpcIdentityCard.class))
|
||||||
|
.satisfies(e -> {
|
||||||
|
Throwable root = e;
|
||||||
|
while (root.getCause() != null) root = root.getCause();
|
||||||
|
t.line(e.getClass().getName() + ": " + e.getMessage());
|
||||||
|
});
|
||||||
|
t.blank();
|
||||||
|
|
||||||
|
// a @MappedSuperclass is not an entity
|
||||||
|
t.line("== querying a @MappedSuperclass");
|
||||||
|
try (Db db = new Db(AuditedBase.class, Invoice.class, Receipt.class)) {
|
||||||
|
db.sessionFactory.inTransaction(s -> {
|
||||||
|
s.persist(new Invoice("INV-1"));
|
||||||
|
s.persist(new Receipt("paid"));
|
||||||
|
});
|
||||||
|
t.line("tables: " + db.jdbc("select table_name from information_schema.tables where table_schema = 'PUBLIC' and table_name like 'MP\\_%' escape '\\' order by 1").stream().map(String::toLowerCase).toList());
|
||||||
|
assertThatThrownBy(() -> db.sessionFactory.inSession(s -> s.createSelectionQuery("select a from AuditedBase a", AuditedBase.class).getResultList()))
|
||||||
|
.satisfies(e -> t.line(e.getClass().getName() + ": " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import org.hibernate.boot.MetadataSources;
|
||||||
|
import org.hibernate.boot.registry.StandardServiceRegistry;
|
||||||
|
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
|
||||||
|
import org.hibernate.cfg.AvailableSettings;
|
||||||
|
import org.hibernate.resource.jdbc.spi.StatementInspector;
|
||||||
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/** Plain-Hibernate bootstrap against in-memory H2, recording every SQL string Hibernate prepares. */
|
||||||
|
final class Db implements AutoCloseable {
|
||||||
|
|
||||||
|
private static final AtomicInteger SEQ = new AtomicInteger();
|
||||||
|
|
||||||
|
/** Every statement Hibernate prepared since the last {@link #clear()}. */
|
||||||
|
final List<String> sql = new ArrayList<>();
|
||||||
|
final SessionFactory sessionFactory;
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
Db(Class<?>... entities) {
|
||||||
|
// OPTIMIZE_REUSE_RESULTS=0: by default H2 hands back the previous result of an identical query when no table
|
||||||
|
// changed, which makes a repeated-query benchmark report ~0.1 ms for a 150,000-row union.
|
||||||
|
this.url = "jdbc:h2:mem:inh" + SEQ.incrementAndGet() + ";DB_CLOSE_DELAY=-1;OPTIMIZE_REUSE_RESULTS=0";
|
||||||
|
StatementInspector inspector = s -> {
|
||||||
|
sql.add(s);
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
|
||||||
|
.applySetting(AvailableSettings.JAKARTA_JDBC_URL, url)
|
||||||
|
.applySetting(AvailableSettings.JAKARTA_JDBC_USER, "sa")
|
||||||
|
.applySetting(AvailableSettings.JAKARTA_JDBC_PASSWORD, "")
|
||||||
|
.applySetting(AvailableSettings.HBM2DDL_AUTO, "create-drop")
|
||||||
|
.applySetting(AvailableSettings.STATEMENT_INSPECTOR, inspector)
|
||||||
|
.build();
|
||||||
|
MetadataSources sources = new MetadataSources(registry);
|
||||||
|
for (Class<?> e : entities) {
|
||||||
|
sources.addAnnotatedClass(e);
|
||||||
|
}
|
||||||
|
this.sessionFactory = sources.buildMetadata().buildSessionFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
sql.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Raw JDBC against the same in-memory database. */
|
||||||
|
List<String> jdbc(String query) {
|
||||||
|
List<String> rows = new ArrayList<>();
|
||||||
|
try (Connection c = java.sql.DriverManager.getConnection(url, "sa", "");
|
||||||
|
Statement st = c.createStatement();
|
||||||
|
ResultSet rs = st.executeQuery(query)) {
|
||||||
|
int cols = rs.getMetaData().getColumnCount();
|
||||||
|
while (rs.next()) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 1; i <= cols; i++) {
|
||||||
|
if (i > 1) sb.append(" | ");
|
||||||
|
sb.append(rs.getString(i));
|
||||||
|
}
|
||||||
|
rows.add(sb.toString());
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Raw JDBC statement; returns the database's error message if it is rejected, or "accepted". */
|
||||||
|
String tryExec(String statement) {
|
||||||
|
try (Connection c = java.sql.DriverManager.getConnection(url, "sa", "");
|
||||||
|
Statement st = c.createStatement()) {
|
||||||
|
st.executeUpdate(statement);
|
||||||
|
return "accepted";
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
sessionFactory.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DDL Hibernate would run for exactly these entities, as Hibernate itself writes it. Uses the standard
|
||||||
|
* JPA script-generation settings: org.hibernate.tool.hbm2ddl.SchemaExport is not in hibernate-core 7.4.13.
|
||||||
|
*/
|
||||||
|
static List<String> ddl(Class<?>... entities) {
|
||||||
|
try {
|
||||||
|
Path out = Files.createTempFile("ddl", ".sql");
|
||||||
|
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
|
||||||
|
.applySetting(AvailableSettings.JAKARTA_JDBC_URL, "jdbc:h2:mem:ddl" + SEQ.incrementAndGet() + ";DB_CLOSE_DELAY=-1")
|
||||||
|
.applySetting(AvailableSettings.JAKARTA_JDBC_USER, "sa")
|
||||||
|
.applySetting(AvailableSettings.JAKARTA_JDBC_PASSWORD, "")
|
||||||
|
.applySetting(AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, "none")
|
||||||
|
.applySetting(AvailableSettings.JAKARTA_HBM2DDL_SCRIPTS_ACTION, "create")
|
||||||
|
.applySetting(AvailableSettings.JAKARTA_HBM2DDL_SCRIPTS_CREATE_TARGET, out.toString())
|
||||||
|
.applySetting(AvailableSettings.FORMAT_SQL, "true")
|
||||||
|
.build();
|
||||||
|
MetadataSources sources = new MetadataSources(registry);
|
||||||
|
for (Class<?> e : entities) {
|
||||||
|
sources.addAnnotatedClass(e);
|
||||||
|
}
|
||||||
|
sources.buildMetadata().buildSessionFactory().close();
|
||||||
|
List<String> lines = Files.readAllLines(out);
|
||||||
|
Files.delete(out);
|
||||||
|
return lines;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/** Writes output/01-ddl-*.txt: the schema Hibernate generates for each strategy. */
|
||||||
|
class DdlTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ddlPerStrategy() {
|
||||||
|
for (Strategies s : Strategies.values()) {
|
||||||
|
List<String> ddl = Db.ddl(s.classes());
|
||||||
|
long tables = ddl.stream().filter(l -> l.stripLeading().startsWith("create table")).count();
|
||||||
|
// the contract this chapter relies on: how many tables each strategy creates for 4 payment classes + 1 order
|
||||||
|
assertThat(tables).as(s.label).isEqualTo(switch (s) {
|
||||||
|
case SINGLE_TABLE -> 2L; // st_payment, st_order
|
||||||
|
case JOINED -> 5L; // jn_payment, jn_card_payment, jn_bank_transfer_payment, jn_wallet_payment, jn_order
|
||||||
|
case TABLE_PER_CLASS -> 4L; // three concrete tables + tp_order; the abstract root has none
|
||||||
|
});
|
||||||
|
try (Transcript t = new Transcript("01-ddl-" + s.label)) {
|
||||||
|
t.line("-- DDL generated by Hibernate " + org.hibernate.Version.getVersionString() + " for H2, strategy " + s.name());
|
||||||
|
t.line("-- tables created: " + tables);
|
||||||
|
t.blank();
|
||||||
|
ddl.forEach(t::line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mappedSuperclassDdl() {
|
||||||
|
List<String> ddl = Db.ddl(com.ankurm.inheritance.mapped.AuditedBase.class,
|
||||||
|
com.ankurm.inheritance.mapped.Invoice.class, com.ankurm.inheritance.mapped.Receipt.class);
|
||||||
|
// two entities, two tables: the @MappedSuperclass contributes columns but no table of its own
|
||||||
|
assertThat(ddl.stream().filter(l -> l.stripLeading().startsWith("create table")).count()).isEqualTo(2);
|
||||||
|
try (Transcript t = new Transcript("01-ddl-mapped-superclass")) {
|
||||||
|
t.line("-- DDL generated by Hibernate " + org.hibernate.Version.getVersionString() + " for H2, @MappedSuperclass AuditedBase + Invoice + Receipt");
|
||||||
|
t.blank();
|
||||||
|
ddl.forEach(t::line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity(name = "SnA")
|
||||||
|
@DiscriminatorValue("A")
|
||||||
|
class SnA extends SnRoot {
|
||||||
|
|
||||||
|
@Column(name = "a_value", nullable = false)
|
||||||
|
String aValue = "a";
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity(name = "SnB")
|
||||||
|
@DiscriminatorValue("B")
|
||||||
|
class SnB extends SnRoot {
|
||||||
|
|
||||||
|
@Column(name = "b_value", nullable = false)
|
||||||
|
String bValue = "b";
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
/** Deliberately wrong: SINGLE_TABLE where each subclass declares a mandatory column. Only ContractsTest uses it. */
|
||||||
|
@Entity(name = "SnRoot")
|
||||||
|
@Table(name = "sn_root")
|
||||||
|
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
|
||||||
|
@DiscriminatorColumn(name = "kind", length = 4)
|
||||||
|
abstract class SnRoot {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||||
|
Long id;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import org.hibernate.Session;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/** Writes output/02-sql-*.txt: the INSERTs and SELECTs Hibernate emits for the same four operations on each strategy. */
|
||||||
|
class SqlTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sqlPerStrategy() {
|
||||||
|
for (Strategies s : Strategies.values()) {
|
||||||
|
try (Db db = new Db(s.classes()); Transcript t = new Transcript("02-sql-" + s.label)) {
|
||||||
|
t.line("-- strategy " + s.name() + ", Hibernate " + org.hibernate.Version.getVersionString());
|
||||||
|
t.blank();
|
||||||
|
|
||||||
|
// 1. persist one CardPayment, one BankTransferPayment, one WalletPayment, one order
|
||||||
|
List<Object> sample = s.sample();
|
||||||
|
db.sessionFactory.inTransaction(session -> sample.forEach(session::persist));
|
||||||
|
List<String> inserts = db.sql.stream().filter(q -> q.startsWith("insert")).toList();
|
||||||
|
t.line("== persist 3 payments and 1 order: " + inserts.size() + " INSERT statements");
|
||||||
|
inserts.forEach(q -> t.sql("insert", q));
|
||||||
|
t.blank();
|
||||||
|
|
||||||
|
// 2. polymorphic query over the root
|
||||||
|
db.clear();
|
||||||
|
try (Session session = db.sessionFactory.openSession()) {
|
||||||
|
List<?> all = session.createSelectionQuery("select p from " + s.prefix + "Payment p order by p.id", Object.class).getResultList();
|
||||||
|
assertThat(all).hasSize(3);
|
||||||
|
t.line("== select p from " + s.prefix + "Payment p (polymorphic, returns 3 rows)");
|
||||||
|
db.sql.stream().filter(q -> q.startsWith("select")).forEach(q -> t.sql("select", q));
|
||||||
|
}
|
||||||
|
t.blank();
|
||||||
|
|
||||||
|
// 3. query a single subclass
|
||||||
|
db.clear();
|
||||||
|
try (Session session = db.sessionFactory.openSession()) {
|
||||||
|
List<?> cards = session.createSelectionQuery("from " + s.prefix + "CardPayment", Object.class).getResultList();
|
||||||
|
assertThat(cards).hasSize(1);
|
||||||
|
t.line("== from " + s.prefix + "CardPayment (one subclass, returns 1 row)");
|
||||||
|
db.sql.stream().filter(q -> q.startsWith("select")).forEach(q -> t.sql("select", q));
|
||||||
|
}
|
||||||
|
t.blank();
|
||||||
|
|
||||||
|
// 4. an association that points at the root
|
||||||
|
db.clear();
|
||||||
|
try (Session session = db.sessionFactory.openSession()) {
|
||||||
|
List<?> orders = session.createSelectionQuery("select o from " + s.prefix + "Order o join fetch o.payment", Object.class).getResultList();
|
||||||
|
assertThat(orders).hasSize(1);
|
||||||
|
t.line("== select o from " + s.prefix + "Order o join fetch o.payment (association to the root)");
|
||||||
|
db.sql.stream().filter(q -> q.startsWith("select")).forEach(q -> t.sql("select", q));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** The three hierarchies side by side: same shape, different strategy, different package and entity-name prefix. */
|
||||||
|
enum Strategies {
|
||||||
|
|
||||||
|
SINGLE_TABLE("single-table", "St", "st") {
|
||||||
|
@Override
|
||||||
|
Class<?>[] classes() {
|
||||||
|
return new Class<?>[]{
|
||||||
|
com.ankurm.inheritance.single.Payment.class,
|
||||||
|
com.ankurm.inheritance.single.CardPayment.class,
|
||||||
|
com.ankurm.inheritance.single.BankTransferPayment.class,
|
||||||
|
com.ankurm.inheritance.single.WalletPayment.class,
|
||||||
|
com.ankurm.inheritance.single.PaymentOrder.class};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
List<Object> sample() {
|
||||||
|
var card = new com.ankurm.inheritance.single.CardPayment(new BigDecimal("49.90"), "EUR", "4242", "VISA");
|
||||||
|
var bank = new com.ankurm.inheritance.single.BankTransferPayment(new BigDecimal("1200.00"), "EUR", "DE89370400440532013000", "COBADEFF");
|
||||||
|
var wallet = new com.ankurm.inheritance.single.WalletPayment(new BigDecimal("15.00"), "EUR", "PAYPAL");
|
||||||
|
return List.of(card, bank, wallet, new com.ankurm.inheritance.single.PaymentOrder("ORD-1", card));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
JOINED("joined", "Jn", "jn") {
|
||||||
|
@Override
|
||||||
|
Class<?>[] classes() {
|
||||||
|
return new Class<?>[]{
|
||||||
|
com.ankurm.inheritance.joined.Payment.class,
|
||||||
|
com.ankurm.inheritance.joined.CardPayment.class,
|
||||||
|
com.ankurm.inheritance.joined.BankTransferPayment.class,
|
||||||
|
com.ankurm.inheritance.joined.WalletPayment.class,
|
||||||
|
com.ankurm.inheritance.joined.PaymentOrder.class};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
List<Object> sample() {
|
||||||
|
var card = new com.ankurm.inheritance.joined.CardPayment(new BigDecimal("49.90"), "EUR", "4242", "VISA");
|
||||||
|
var bank = new com.ankurm.inheritance.joined.BankTransferPayment(new BigDecimal("1200.00"), "EUR", "DE89370400440532013000", "COBADEFF");
|
||||||
|
var wallet = new com.ankurm.inheritance.joined.WalletPayment(new BigDecimal("15.00"), "EUR", "PAYPAL");
|
||||||
|
return List.of(card, bank, wallet, new com.ankurm.inheritance.joined.PaymentOrder("ORD-1", card));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TABLE_PER_CLASS("table-per-class", "Tp", "tp") {
|
||||||
|
@Override
|
||||||
|
Class<?>[] classes() {
|
||||||
|
return new Class<?>[]{
|
||||||
|
com.ankurm.inheritance.tpc.Payment.class,
|
||||||
|
com.ankurm.inheritance.tpc.CardPayment.class,
|
||||||
|
com.ankurm.inheritance.tpc.BankTransferPayment.class,
|
||||||
|
com.ankurm.inheritance.tpc.WalletPayment.class,
|
||||||
|
com.ankurm.inheritance.tpc.PaymentOrder.class};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
List<Object> sample() {
|
||||||
|
var card = new com.ankurm.inheritance.tpc.CardPayment(new BigDecimal("49.90"), "EUR", "4242", "VISA");
|
||||||
|
var bank = new com.ankurm.inheritance.tpc.BankTransferPayment(new BigDecimal("1200.00"), "EUR", "DE89370400440532013000", "COBADEFF");
|
||||||
|
var wallet = new com.ankurm.inheritance.tpc.WalletPayment(new BigDecimal("15.00"), "EUR", "PAYPAL");
|
||||||
|
return List.of(card, bank, wallet, new com.ankurm.inheritance.tpc.PaymentOrder("ORD-1", card));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String prefix;
|
||||||
|
final String tablePrefix;
|
||||||
|
|
||||||
|
Strategies(String label, String prefix, String tablePrefix) {
|
||||||
|
this.label = label;
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.tablePrefix = tablePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract Class<?>[] classes();
|
||||||
|
|
||||||
|
/** One card, one bank transfer, one wallet payment, and an order pointing at the card payment. */
|
||||||
|
abstract List<Object> sample();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity(name = "TpcIdentityCard")
|
||||||
|
class TpcIdentityCard extends TpcIdentityPayment {
|
||||||
|
|
||||||
|
String cardLast4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
/** Deliberately wrong: TABLE_PER_CLASS with IDENTITY. Only ContractsTest touches this; it never joins the main mappings. */
|
||||||
|
@Entity(name = "TpcIdentityPayment")
|
||||||
|
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
|
||||||
|
abstract class TpcIdentityPayment {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
Long id;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.ankurm.inheritance;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects lines and writes them to output/NN-name.txt when closed. The tests assert the same
|
||||||
|
* numbers they print, so every figure in the transcripts is an assertion that fails the build
|
||||||
|
* when it stops being true.
|
||||||
|
*/
|
||||||
|
final class Transcript implements AutoCloseable {
|
||||||
|
|
||||||
|
private final Path file;
|
||||||
|
private final List<String> lines = new ArrayList<>();
|
||||||
|
|
||||||
|
Transcript(String name) {
|
||||||
|
this.file = Path.of("output", name + ".txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
Transcript line(String s) {
|
||||||
|
lines.add(s);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transcript blank() {
|
||||||
|
lines.add("");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hibernate emits one long line per statement; break it at the keywords so a reader can follow the joins. */
|
||||||
|
Transcript sql(String ignoredLabel, String sql) {
|
||||||
|
String pretty = sql
|
||||||
|
.replace(" union all select", "\nunion all\nselect")
|
||||||
|
.replaceAll(" (left join|inner join|cross join|join|where|group by|order by) ", "\n $1 ")
|
||||||
|
.replaceAll(" from ", "\nfrom ");
|
||||||
|
lines.add(pretty + ";");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(file.getParent());
|
||||||
|
Files.write(file, lines);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user