
Spec
Database Modeling Extension User Manual
Map proto message to database table structure (JPA entity).
Extension Package: apihug/protobuf/domain/persistence.proto + annotations.proto
Scope: Message(table) + Field(column)
Scenarios: JPA entity, DDL generation, Liquibase
NEVER add any file-level option declarations in proto files:
// ❌ Forbidden to use the following options (rely on default code generation configuration)
option java_package = "...";
option java_multiple_files = true;
option go_package = "...";
All code generation configurations are automatically managed by the build system. Strictly forbidden to manually add any file-level options.
Domain layer (domain/) must maintain architectural purity:
// ❌ Forbidden to reference API layer messages
import "com/example/api/user/api.proto"; // Wrong!
import "com/example/api/order/request.proto"; // Wrong!
message UserEntity {
// ❌ Forbidden to use API Request/Response types
CreateUserRequest request = 1; // Wrong!
UserLoginResponse response = 2; // Wrong!
}
✅ Allowed references:
// ✅ Allow referencing constant layer (infra/)
import "com/example/infra/settings/user_constant.proto";
// ✅ Allow referencing other domain entities
import "com/example/domain/entities/order.proto";
message UserEntity {
UserStatusEnum status = 1; // ✅ Enums can be shared
OrderEntity order = 2; // ✅ Domain internal reference
}
Architecture Principles: Domain layer only focuses on data persistence and is completely isolated from API layer.
import "apihug/protobuf/domain/annotations.proto";
Note: annotations.proto automatically imports persistence.proto and apihug/protobuf/extend/common.proto
message EntityName {
option (hope.persistence.table) = {
name: "TableName";
description: "Table description";
// Other configurations...
};
// Field definitions...
}
string field_name = sequence [(hope.persistence.column) = {
name: "ColumnName";
description: "Column description";
nullable: FALSE;
type: VARCHAR;
// Other configurations...
}];
syntax = "proto3";
package com.example.pet.domain;
import "apihug/protobuf/domain/annotations.proto";
message Category {
option (hope.persistence.table) = {
name: "CATEGORY",
description: "Pet category",
wires: [IDENTIFIABLE] // Automatically add id field
};
string name = 1 [(hope.persistence.column) = {
name: "NAME",
description: "Category name",
nullable: false,
length: 32,
unique: true,
type: VARCHAR
}];
}
name (string)CATEGORY, USER_ORDER)name: "PET"description (string)description: "Pet information table"catalog (string)catalog: "pet_db"schema (string)schema: "public"unique_constraints (repeated)Type: UniqueConstraint message
Function: Define unique constraints
Fields:
name (string): Constraint namecolumn_list (repeated string): Column name arrayExample:
unique_constraints: {
name: "UK_PET_NAME_CATEGORY",
column_list: ["name", "category"]
}
indexes (repeated)Type: Index message
Function: Define indexes
Fields:
name (string): Index namecolumn_list (repeated string): Column name arrayunique (bool): Whether unique indexExample:
indexes: {
name: "IDX_PET_NAME",
column_list: ["name"]
}
indexes: {
name: "IDX_CATEGORY_UNIQUE",
column_list: ["category"],
unique: TRUE
}
wires (repeated Wire enum)Supported WIRE type definitions, strictly prohibited to design similar fields, maintain consistency!!
Function: Enable platform built-in common features
Optional values:
| Enum Value | Meaning | Auto-added Fields |
|---|---|---|
ALL | Enable all features | id + audit + delete flag + tenant + version |
IDENTIFIABLE | Only add primary key | id (Long) |
AUDITABLE | Audit fields | created_at, updated_at, created_by, updated_by |
DELETABLE | Logical delete | deleted (Boolean) |
TENANTABLE | Multi-tenant | tenant_id (Long) |
VERSIONABLE | Optimistic lock | version (Integer) |
NONE | Fully custom | None |
Example:
// Only need primary key
wires: [IDENTIFIABLE]
// Need audit and logical delete
wires: [IDENTIFIABLE, AUDITABLE, DELETABLE]
// Fully custom
wires: [NONE]
views (repeated View message)Warning: ⚠️ Do not add views unless explicitly required by users!
Function: Define different views for entity (list, detail, statistics, etc.)
Fields:
name (string): View namedescription (string): View descriptionincludes (repeated string): Included field listexcludes (repeated string): Excluded field listreferences (repeated ReferenceView): Associated other entitiesaggregated_fields (repeated AggregatedField): Aggregated fieldsExample:
views: [{
name: "PetView",
description: "Pet list view",
includes: ["id", "name", "category", "size"]
}]
views: [{
name: "PetDetailView",
description: "Pet detail view (with category info)",
includes: ["id", "name", "category"],
references: [{
entity: "Category",
includes: ["description"],
join_type: INNER,
left_field: "category",
right_field: "name"
}]
}]
liquibase (repeated Liquibase message)Function: Execute custom SQL for specific databases
Fields:
version (uint32): Version number (append only, cannot modify)comment (string): Change descriptiondbms (repeated DBMS enum): Target databasenegative (bool): Whether to exclude dbms (true means exclude)sql (repeated string): SQL statement arrayExample:
liquibase: {
version: 1,
comment: "MySQL special index",
dbms: [MYSQL],
sql: [
"CREATE FULLTEXT INDEX idx_pet_name_ft ON PET(name)"
]
}
liquibase: {
version: 2,
comment: "Partitioned table for non-H2 databases",
dbms: [H2],
negative: true,
sql: [
"ALTER TABLE PET PARTITION BY RANGE (id)"
]
}
name (string)USER_NAME, CREATED_AT)name: "NAME"description (string)description: "User name"nullable (bool)Type: bool
Function: Whether to allow NULL
Optional values:
true: Allow NULLfalse: NOT NULLExample:
nullable: false // NOT NULL
nullable: true // Allow NULL
unique (bool)Type: bool
Function: Whether unique constraint
Example:
unique: true // UNIQUE
unique: false // Not unique
insertable (bool)Function: Whether to participate in INSERT statement
Example:
insertable: false // Do not participate in insert (e.g., calculated fields)
updatable (bool)Function: Whether to participate in UPDATE statement
Example:
updatable: false // Cannot be modified (e.g., creation time)
searchable (bool)Function: Whether used for WHERE condition query
Example:
searchable: true // Can be used for filtering
sortable (bool)Function: Whether used for ORDER BY sorting
Example:
sortable: true // Can sort
type (Column.Type enum)Type: hope.persistence.Column.Type enum
Function: Explicitly specify SQL type (override proto type inference)
Common values:
| Enum Value | SQL Type | Use Case |
|---|---|---|
VARCHAR | VARCHAR | String |
CHAR | CHAR | Fixed-length character |
INTEGER | INTEGER | Integer |
BIGINT | BIGINT | Long integer |
DOUBLE | DOUBLE | Float |
DECIMAL | DECIMAL | Precise decimal |
DATE | DATE | Date |
TIME | TIME | Time |
TIMESTAMP | TIMESTAMP | Timestamp |
BOOLEAN | BOOLEAN | Boolean |
BLOB | BLOB | Binary large object |
CLOB | CLOB | Character large object |
Example:
string name = 1 [(hope.persistence.column) = {
type: VARCHAR // Explicitly specify VARCHAR
}];
double weight = 2 [(hope.persistence.column) = {
type: DOUBLE // Float
}];
length (uint32)Type: uint32
Function: String length (only valid for VARCHAR/CHAR)
Example:
length: 32 // VARCHAR(32)
length: 255 // VARCHAR(255)
precision (uint32)Type: uint32
Function: DECIMAL total digits
Example:
precision: 10 // DECIMAL(10, ...)
scale (uint32)Type: uint32
Function: DECIMAL decimal digits
Example:
precision: 10,
scale: 2 // DECIMAL(10, 2)
enum_type (EnumType enum)Type: hope.persistence.EnumType enum
Function: Persistence method for enum fields
Optional values:
| Enum Value | Persisted Content | Description |
|---|---|---|
STRING | Enum name | Default value, stores like “AVAILABLE” |
CODE | code field | Stores business code (needs to define code in enum) |
Forbidden: ❌ No longer supports ORDINAL (sequence number), deprecated due to errors
Example:
import "com/example/enumeration/constants.proto";
com.example.enumeration.Size size = 1 [(hope.persistence.column) = {
name: "SIZE",
enum_type: STRING, // Store "SMALL"/"MEDIUM"/"LARGE"
type: VARCHAR
}];
com.example.enumeration.Status status = 2 [(hope.persistence.column) = {
name: "STATUS",
enum_type: CODE, // Store 1/2/3 (enum code values)
type: INTEGER
}];
id (bool)Type: bool
Function: Mark as primary key
Example:
int64 id = 1 [(hope.persistence.column) = {
name: "ID",
id: true,
nullable: false
}];
generated_value (GeneratedValue message)Function: Primary key generation strategy
Fields:
strategy (GenerationType enum): Generation strategygenerator (string): Generator name (optional)Strategy enum:
| Enum Value | Description | Use Case |
|---|---|---|
TABLE | Table sequence | Generic |
SEQUENCE | Database sequence | Oracle/PostgreSQL |
IDENTITY | Auto-increment column | MySQL/SQL Server |
UUID | UUID generation | Distributed systems |
AUTO | Auto select | Let framework decide |
Example:
int64 id = 1 [(hope.persistence.column) = {
name: "ID",
id: TRUE,
generated_value: {
strategy: UUID
}
}];
int64 order_id = 1 [(hope.persistence.column) = {
name: "ORDER_ID",
id: TRUE,
generated_value: {
strategy: SEQUENCE,
generator: "order_seq"
}
}];
transient (bool)Function: Mark as non-persistent field (view or calculated field only)
Example:
string full_name = 10 [(hope.persistence.column) = {
transient: true // Do not map to database
}];
default_value (string)Function: Column default value (SQL level)
Example:
string status = 5 [(hope.persistence.column) = {
name: "STATUS",
default_value: "ACTIVE"
}];
int32 retry_count = 6 [(hope.persistence.column) = {
name: "RETRY_COUNT",
default_value: "0"
}];
column_definition (string)Function: Fully customize column DDL (override all auto-generation)
Warning: ⚠️ Use only for special needs
Example:
string name = 1 [(hope.persistence.column) = {
column_definition: "VARCHAR(32) NOT NULL COMMENT 'Name'"
}];
table (string)Function: Specify which table the field belongs to (auxiliary table scenario)
Example:
string extra_info = 10 [(hope.persistence.column) = {
name: "EXTRA_INFO",
table: "PET_DETAIL" // Field in PET_DETAIL table
}];
Wrong: ❌ The following syntax is wrong
nullable: "false" // String, wrong!
unique: "true" // String, wrong!
Correct: ✅ Must use bool type
nullable: false // bool type
unique: true // bool type
Wrong: ❌ The following syntax is outdated
length: {value: 32} // Old syntax, no longer needed!
precision: {value: 10} // Old syntax, no longer needed!
Correct: ✅ Use uint32 directly
length: 32 // uint32 type
precision: 10
scale: 2
proto field type vs SQL type mapping:
| proto type | Default SQL | Explicit type specification |
|---|---|---|
string | VARCHAR | type: VARCHAR / CHAR / CLOB |
int32 | INTEGER | type: INTEGER / SMALLINT |
int64 | BIGINT | type: BIGINT |
double | DOUBLE | type: DOUBLE / DECIMAL |
float | FLOAT | type: FLOAT |
bool | BOOLEAN | type: BOOLEAN / BIT |
bytes | BLOB | type: BLOB / VARBINARY |
| enum | VARCHAR | type: VARCHAR (with enum_type) |
Example:
// proto string, default VARCHAR
string name = 1 [(hope.persistence.column) = {
name: "NAME"
// No type specified, auto-inferred as VARCHAR
}];
// proto string, force CHAR
string code = 2 [(hope.persistence.column) = {
name: "CODE",
type: CHAR, // Explicitly specify CHAR
length: 10
}];
// proto double, force DECIMAL
double price = 3 [(hope.persistence.column) = {
name: "PRICE",
type: DECIMAL, // Explicitly specify DECIMAL
precision: 10,
scale: 2
}];
syntax = "proto3";
package com.example.pet.domain;
import "domain/annotations.proto";
import "com/example/pet/enumeration/constants.proto";
message Pet {
option (hope.persistence.table) = {
name: "PET",
description: "Pet information table",
unique_constraints: {
name: "UK_PET_NAME_CATEGORY",
column_list: ["name", "category"]
},
indexes: {
name: "IDX_PET_NAME",
column_list: ["name"]
},
wires: [IDENTIFIABLE, AUDITABLE, DELETABLE]
};
string name = 1 [(hope.persistence.column) = {
name: "NAME",
description: "Pet name",
nullable: false,
length: 32,
type: VARCHAR
}];
string category = 3 [(hope.persistence.column) = {
name: "CATEGORY",
description: "Category (associated CATEGORY#NAME)",
nullable: false,
length: 32,
type: VARCHAR
}];
com.example.pet.enumeration.Size size = 4 [(hope.persistence.column) = {
name: "SIZE",
description: "Pet size",
nullable: FALSE,
enum_type: STRING, // Store "SMALL"/"MEDIUM"/"LARGE"
type: VARCHAR,
length: 32
}];
double weight = 5 [(hope.persistence.column) = {
name: "WEIGHT",
description: "Pet weight(kg)",
nullable: FALSE,
type: DOUBLE
}];
}
syntax = "proto3";
package com.example.order.domain;
import "domain/annotations.proto";
import "com/example/order/enumeration/constants.proto";
message Order {
option (hope.persistence.table) = {
name: "USER_ORDER",
description: "Order table",
indexes: {
name: "IDX_ORDER_USER",
column_list: ["user_id"]
},
indexes: {
name: "IDX_ORDER_STATUS",
column_list: ["status"]
},
wires: [ALL] // Enable all features
};
// Primary key
int64 id = 1 [(hope.persistence.column) = {
name: "ID",
description: "Order ID",
id: true,
nullable: false,
generated_value: {
strategy: UUID
}
}];
// Foreign key
int64 user_id = 2 [(hope.persistence.column) = {
name: "USER_ID",
description: "User ID",
nullable: FALSE,
searchable: TRUE,
type: BIGINT
}];
// Enum (store code)
com.example.order.enumeration.OrderStatus status = 3 [(hope.persistence.column) = {
name: "STATUS",
description: "Order status",
nullable: FALSE,
enum_type: CODE, // Store 1/2/3... (code value)
type: INTEGER
}];
// Amount (DECIMAL)
double total_amount = 4 [(hope.persistence.column) = {
name: "TOTAL_AMOUNT",
description: "Order total amount",
nullable: FALSE,
type: DECIMAL,
precision: 10,
scale: 2
}];
// Remark (nullable long text)
string remark = 5 [(hope.persistence.column) = {
name: "REMARK",
description: "Order remark",
nullable: TRUE,
length: 500,
type: VARCHAR
}];
// Non-persistent calculated field
string user_name = 10 [(hope.persistence.column) = {
transient: TRUE
}];
}
message Pet {
option (hope.persistence.table) = {
name: "PET",
description: "Pet",
// List view
views: [{
name: "PetListView",
description: "Pet list view",
includes: ["id", "name", "category", "size"]
}],
// Detail view (with category)
views: [{
name: "PetDetailView",
description: "Pet detail view (with category info)",
includes: ["id", "name", "category"],
references: [{
entity: "Category",
includes: ["description"],
join_type: INNER,
left_field: "category",
right_field: "name"
}]
}],
wires: [IDENTIFIABLE]
};
// Field definitions...
}
❌ Wrong:
nullable: "false" // String, wrong!
length: {value: 32} // Old syntax, no longer needed!
unique: "true" // String, wrong!
✅ Correct:
nullable: false // bool type
length: 32 // uint32 type
unique: true // bool type
❌ Wrong:
com.example.Size size = 1 [(hope.persistence.column) = {
name: "SIZE",
enum_type: ORDINAL, // ORDINAL is deprecated!
type: INTEGER
}];
✅ Correct:
com.example.Size size = 1 [(hope.persistence.column) = {
name: "SIZE",
enum_type: STRING, // Use STRING or CODE
type: VARCHAR,
length: 32
}];
❌ Wrong:
double price = 1 [(hope.persistence.column) = {
type: DECIMAL // Missing precision and scale
}];
✅ Correct:
double price = 1 [(hope.persistence.column) = {
type: DECIMAL,
precision: 10,
scale: 2
}];
❌ Wrong:
int64 id = 1 [(hope.persistence.column) = {
id: TRUE
// Missing nullable and generated_value
}];
✅ Correct:
int64 id = 1 [(hope.persistence.column) = {
name: "ID",
id: true,
nullable: false,
generated_value: {
strategy: UUID
}
}];
USER_ORDER, PET_CATEGORY)USER_ID, CREATED_AT)IDX_table_column (IDX_PET_NAME)UK_table_column (UK_USER_EMAIL)Only need primary key → wires: [IDENTIFIABLE]
Need audit → wires: [IDENTIFIABLE, AUDITABLE]
Need logical delete → wires: [IDENTIFIABLE, DELETABLE]
Multi-tenant → wires: [IDENTIFIABLE, TENANTABLE]
Full features → wires: [ALL]
Fully custom → wires: [NONE]
Short string (<255) → VARCHAR + length
Long text → CLOB
Integer primary key → BIGINT
UUID primary key → VARCHAR(36) or framework handling
Amount → DECIMAL(10,2)
Date → DATE
Timestamp → TIMESTAMP
Boolean → BOOLEAN
Enum → VARCHAR + enum_type:STRING
syntax = "proto3";
package your_package;
import "domain/annotations.proto";
message EntityName {
option (hope.persistence.table) = {
name: "TABLE_NAME",
description: "Description",
wires: [IDENTIFIABLE]
};
field_type field_name = sequence [(hope.persistence.column) = {
name: "COLUMN_NAME",
description: "Description",
nullable: FALSE,
type: SQL_TYPE
}];
}
int64 id = 1 [(hope.persistence.column) = {
name: "ID",
id: true,
nullable: false,
generated_value: {
strategy: UUID
}
}];
import "your_enum_path/constants.proto";
your_package.EnumName field_name = sequence [(hope.persistence.column) = {
name: "COLUMN_NAME",
nullable: FALSE,
enum_type: STRING,
type: VARCHAR,
length: 32
}];
double field_name = sequence [(hope.persistence.column) = {
name: "COLUMN_NAME",
nullable: FALSE,
type: DECIMAL,
precision: 10,
scale: 2
}];