From CREATE TABLE to JPA Entity: the type mapping
Transcribing DDL into a JPA Entity is mechanical work full of small
decisions: which Java type for NUMERIC? IDENTITY or
SEQUENCE? Does nullable go in the annotation or do you
trust the database? Here's the mapping Schemint uses and the reasoning behind
each choice.
PostgreSQL to Java type table
| PostgreSQL | Java | Note |
|---|---|---|
BIGSERIAL / BIGINT | Long | generated id: @GeneratedValue(strategy = IDENTITY) |
SERIAL / INTEGER | Integer | |
SMALLINT | Short | |
VARCHAR(n) / TEXT | String | @Column(length = n) when there's a limit |
NUMERIC(p,s) | BigDecimal | never double for money |
BOOLEAN | Boolean | |
TIMESTAMP | LocalDateTime | a wall-clock reading, no zone |
TIMESTAMPTZ | Instant / OffsetDateTime | a real instant, normalized to UTC |
DATE | LocalDate | |
TIME | LocalTime | |
UUID | UUID | |
BYTEA | byte[] | |
JSONB | String | or @JdbcTypeCode(SqlTypes.JSON) with your own type |
Wrappers, and the boolean trap
Wrapper types (Long, not long): a nullable column
has to be able to represent absence, and a primitive with a magic 0 value is a
bug waiting to happen. The sneakiest case is boolean: map a
nullable BOOLEAN column to the primitive and Hibernate has to
unbox null on read, which throws deep inside entity hydration,
far from the line that caused it. The safe rule is mechanical: column is
NOT NULL, primitive is allowed; column is nullable,
wrapper is required. Schemint just uses wrappers everywhere so the
generated class never depends on remembering the rule.
The generated skeleton
From the sample DDL (customers with name, unique email and
audit columns), the full entity comes out as:
@Entity
@Table(name = "customers")
public class CustomerEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 120)
private String name;
@Column(nullable = false, unique = true, length = 180)
private String email;
@Column(length = 20)
private String document;
@Column(nullable = false)
private Boolean active = true;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
- IDENTITY, not AUTO: with a Postgres
BIGSERIAL/identity column,IDENTITYreflects what the database actually does. AUTO lets Hibernate pick and can end up on a sequence table. If the table takes bulk inserts, IDENTITY has a batching cost worth knowing about, IDENTITY vs SEQUENCE is its own decision. - nullable/unique/length in the annotation: they don't
change runtime (the database is what validates), but they document the
contract on the class and let Hibernate validate the schema at boot
(
hibernate.ddl-auto=validate), which catches entity/DDL drift on the first deploy instead of on the first query. - The DDL DEFAULT becomes a field initializer:
active BOOLEAN DEFAULT truecomes out asprivate Boolean active = true;, so an entity created in Java and a row created in SQL agree on the initial state. - FK as a plain column first: generating
customer_id Longinstead of@ManyToOneavoids deciding fetch/cascade automatically; a relationship is a modeling decision, not a transcription one. Adding the association later is additive; undoing a generated EAGER fetch that leaked into production is not.
What travels with the entity
The entity is rarely alone. The same column list yields the request record
(everything except the id, generated fields and audit timestamps, because
clients don't send those), the response record (everything), and the
JpaRepository<CustomerEntity, Long>. Generating the four
together keeps them agreeing on names and types, which is exactly the part that
drifts when they're written by hand across three files on different days.