The JPA @Table annotation lets you set name, schema, catalog and uniqueConstraints. Which of those affect the SQL a provider emits at runtime, and which only matter when it generates DDL?
answer
- name/schema/catalog → every statement (runtime)
- uniqueConstraints/indexes → CREATE TABLE only
- provider never checks uniqueness in memory
- violation lands at flush/commit, wrapped as constraint violation
- query-then-insert is racy; the index is the arbiter
basics
~20 sname, schema and catalog become the qualified table name in every statement, so they are runtime. uniqueConstraints and indexes are consumed only by schema generation — the provider never enforces them in memory, so violations surface as database errors at flush.
solid answer
~50 s`@Table(name = …, schema = …, catalog = …)` is runtime metadata: those three build the qualified table name that appears in every `SELECT`, `INSERT`, `UPDATE` and `DELETE` for the entity. Get them wrong and nothing works. `uniqueConstraints` and `indexes` are **DDL-only**. They are read by the schema-generation tool and turned into `CREATE TABLE` clauses. If your schema is owned by a migration tool, they are pure documentation — and documentation that silently drifts out of date. The provider never checks uniqueness in memory: it happily lets you persist two entities with the same values, and the database rejects the second one at flush time, which may be at commit, far from the offending line, wrapped as a constraint-violation exception. Practically: keep `name` in the annotation, avoid hardcoding `schema`/`catalog` (set a default in configuration so environments and tenants can differ), and treat constraints in `@Table` as a mirror of the migration, never as the source of truth.
go deeper
Know that @Table sets the table name and is optional, and that schema generation is what turns uniqueConstraints into DDL.
Separate the runtime attributes from the DDL-only ones and explain that uniqueness is enforced by the database, not the provider.
Talk about failure timing at flush versus commit, translating constraint violations by name, the raciness of pre-check queries, and drift between annotations and migrations.
Argue for a single source of truth for schema — migrations — with startup schema validation as the drift detector, and for keeping environment-specific identifiers out of compiled artifacts.
## The two jobs of @Table `@Table` mixes two very different kinds of metadata, and conflating them causes real production surprises. **Identification metadata — runtime.** `name`, `schema` and `catalog` tell the provider where the entity's rows live. They are resolved once at bootstrap into a qualified table name and stamped into every statement generated for that entity. If `name` is absent, it defaults to the entity name run through the naming strategy. If `schema` and `catalog` are absent, the provider falls back to a configured default schema/catalog, and failing that to whatever the connection's default is. **Schema-shape metadata — DDL only.** `uniqueConstraints` and `indexes` describe what the table should look like *if the provider is asked to create it*. They influence `CREATE TABLE` output and nothing else. With schema generation disabled — which is the normal state of any system with a migration tool — they have no effect at all on behaviour. ## Why the distinction bites A developer adds `@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"tenant_id", "email"}))` and believes duplicates are now prevented. They are not, in two separate ways. First, if migrations own the schema, no such constraint exists in the database at all; the annotation created nothing. The application will cheerfully store duplicates until someone notices the data. Second, even when the constraint *does* exist, the provider does not consult it. Uniqueness is enforced by the database, so it fails at the moment the `INSERT` reaches the database — that is, at flush, which for a typical unit of work is at commit. The exception surfaces far from the code that called `persist()`, often after other work in the same transaction, and it arrives as a constraint-violation exception wrapped in a persistence exception rather than a domain-shaped error. Mapping it back to a user-facing "that email is taken" message requires catching it and inspecting the constraint name — which is why explicitly naming your constraints is worth doing, instead of accepting the provider's generated hashed name. And the pre-check people reach for instead — query first, then insert — is racy: two concurrent transactions both find nothing and both insert. The unique index is the only reliable arbiter; the query is at best an optimisation for the friendly error path. ## Hardcoding schema and catalog Putting `schema = "sales"` in the annotation compiles the environment into the class file. It breaks the moment you want the same code against a different schema name in test, or per-tenant schemas, or a database where the concept maps differently. The portable approach is to leave `schema` out of the annotation and set a default at the persistence-unit level, so one configuration property moves the whole application. Reserve the annotation attribute for the genuine case where *this one entity* lives somewhere the others do not. The same argument applies to `catalog`, which additionally means different things on different databases — a real container on some, a synonym for the database on others, and unsupported on yet others. ## Naming and quoting If the table name collides with a reserved word (`user`, `order`, `group`), you must quote it — in JPA by wrapping the name in double quotes inside the annotation value, or by enabling globally quoted identifiers. Quoted identifiers usually become case-sensitive, which then forces every hand-written SQL statement to quote them too. The cheaper resolution is normally to rename the table. ## Practical guidance - Keep `name` explicit. Relying on the naming strategy means a rename of the class silently renames the table. - Leave `schema`/`catalog` out unless the entity is genuinely elsewhere; configure the default instead. - If migrations own the schema, decide as a team whether `uniqueConstraints`/`indexes` are kept as a mirror or dropped. Keeping them has documentation value but creates a second, unverified source of truth; drift is only caught if you run schema validation on startup, which is the check actually worth enabling. - Name constraints explicitly when you do declare them, so the failure at flush can be translated into a meaningful error. - Remember the ordering consequence: because uniqueness is enforced at insert time, batching and flush ordering determine *when* you find out. Calling `flush()` deliberately after a risky insert is a legitimate way to fail early inside your own code rather than at commit.
- If uniqueConstraints in @Table is DDL-only, how should an application handle a duplicate-key situation?Rely on a unique index that migrations actually create, then catch the constraint-violation exception at flush and translate it using the constraint name into a domain error. A pre-check query can be added for a friendlier message but must not be the enforcement mechanism, because two concurrent transactions can both pass it. Calling flush() deliberately after the insert lets you catch the failure at a point you control rather than at commit.
- Why is hardcoding schema in @Table usually a bad idea?It bakes an environment-specific value into the compiled class, so the same artifact cannot run against a differently named schema in test, in another environment, or in a schema-per-tenant setup. Configuring a default schema at the persistence-unit level moves that decision to one property. Reserve the annotation attribute for entities that genuinely live outside the default.
saying these in an interview costs you the question
- Believing uniqueConstraints in @Table prevents duplicates even when migrations own the schema.
- Expecting the provider to detect a uniqueness violation before the SQL reaches the database.
- Using a select-then-insert check as the actual guarantee of uniqueness.
- Hardcoding schema in every entity instead of configuring a default.
- Assuming @Table is mandatory or that omitting name breaks the mapping.