Skip to content

Entity Definition

Basic @QfEntity Declaration

Entities are declared using the @QfEntity annotation on a class.

java
@QfEntity(
    appKey = "app",   // client app key (optional; defaults to the single declared @QfClientApp)
    name = @QfI18n(
        defaultMessage = "Product",
        texts = { @QfI18nText(locale = "ko", message = "상품") }
    )
)
public class ProductEntity {
    // field declarations...
}

@QfEntity Key Attributes

AttributeTypeRequiredDescriptionDefault
appKeyStringOwning client app key"" (uses the single declared app)
name@QfI18nLocalized display name@QfI18n(texts = {})
autoHistoryEnabledbooleanEnable automatic change historyfalse
treePolicy@QfTreePolicyEnable and configure tree structure@QfTreePolicy (disabled)
ownerPolicy@QfOwnerPolicyOwnership and visibility policy (org or user)@QfOwnerPolicy(enabled = false)
deletePolicy@QfCrudPolicyDelete operation policy@QfCrudPolicy(enabled = false)
capabilityKeyStringCapability key for access control"" (derived from class name)
excelDownloadablebooleanEnable Excel exporttrue
excelUploadablebooleanEnable Excel importtrue

Attribute Declaration

Fields are managed by placing display and behavior annotations directly on them. There is no @QfField marker annotation — each annotation controls a specific concern.

java
@QfEntity(
    name = @QfI18n(defaultMessage = "Product", texts = {})
)
public class ProductEntity {

    // List + detail + create + update
    @QfI18n(defaultMessage = "Product Name")
    @QfListAttribute(sortable = true)
    @QfDetailAttribute
    @QfCreateAttribute(requiredOn = @QfRequiredOn(always = true))
    @QfUpdateAttribute
    private String name;

    // List + detail only (read-only price)
    @QfI18n(defaultMessage = "Price")
    @QfListAttribute
    @QfDetailAttribute
    private Integer price;

    // Detail + create + update with textarea control
    @QfI18n(defaultMessage = "Description")
    @QfDetailAttribute
    @QfCreateAttribute
    @QfUpdateAttribute
    @QfControlType(QfControlType.Type.textarea)
    private String description;
}

Display Annotations

AnnotationDescription
@QfI18nDeclare localized field label (placed directly on the field)
@QfListAttributeDisplay as a column in the list view
@QfDetailAttributeDisplay in the detail view
@QfCreateAttributeDisplay as an input field in the create form
@QfUpdateAttributeDisplay as an input field in the update form
@QfSearchEnable as a search filter in the list view

Required Fields

Required rules are declared inside @QfCreateAttribute or @QfUpdateAttribute via requiredOn:

java
// Always required
@QfI18n(defaultMessage = "Name")
@QfCreateAttribute(requiredOn = @QfRequiredOn(always = true))
@QfUpdateAttribute(requiredOn = @QfRequiredOn(always = true))
private String name;

// Required only when type == 'BUSINESS'
@QfI18n(defaultMessage = "Business Registration Number")
@QfCreateAttribute(
    requiredOn = @QfRequiredOn(
        conditions = @QfConditionExpr(expr = "type == 'BUSINESS'")
    )
)
private String businessNumber;

@QfCrypto Encryption

Declaring @QfCrypto on a sensitive field enables automatic encryption on save and decryption on read.

java
@QfI18n(defaultMessage = "Email")
@QfDetailAttribute
@QfCreateAttribute
@QfCrypto                    // automatic encryption
@QfControlType(QfControlType.Type.email)
private String email;

@QfI18n(defaultMessage = "Phone")
@QfDetailAttribute
@QfCreateAttribute
@QfUpdateAttribute
@QfCrypto
private String phone;

Encrypted Field Restriction

A field with @QfCrypto cannot be combined with @QfSearch. Encrypted data does not support server-side LIKE search. This is enforced as a compile error.


Owner Policy

Ownership-based data filtering is configured on the entity via ownerPolicy. Two ownership models are supported: ORGANIZATION (org hierarchy filtering) and USER (caller identity filtering).

java
// Organization ownership — filter by org scope
@QfEntity(
    name = @QfI18n(defaultMessage = "Order", texts = {}),
    ownerPolicy = @QfOwnerPolicy(
        enabled = true,
        attribute = "orgId"   // field name that holds the organization ID
    )
)
public class OrderEntity {

    private String orgId;    // org filter target (declared in ownerPolicy.attribute)

    @QfI18n(defaultMessage = "Order Number")
    @QfListAttribute
    private String orderNumber;

    @QfI18n(defaultMessage = "Order Amount")
    @QfListAttribute
    private Long amount;
}

// User ownership — filter by creator
@QfEntity(
    name = @QfI18n(defaultMessage = "Memo", texts = {}),
    ownerPolicy = @QfOwnerPolicy(
        enabled = true,
        ownerType = QfOwnerType.USER,
        attribute = "createdBy"
    )
)
public class MemoEntity { }

Entities with owner policy enabled:

  • Automatically filter to the current user's ownership scope on queries
  • Automatically populate the owner ID on creation
  • Require no manual WHERE clause

Capability Key

Link an entity to a Capability by setting capabilityKey:

java
@QfEntity(
    name = @QfI18n(defaultMessage = "Product", texts = {}),
    capabilityKey = "product-management"   // must match a @QfCapability key
)
public class ProductEntity {
    // ...
}

Master-Detail Relationship

Declare that an entity is a detail of a master entity via masterRelation:

java
// Order item (Detail entity)
@QfEntity(
    name = @QfI18n(defaultMessage = "Order Item", texts = {}),
    masterRelation = @QfMasterRelation(
        enabled = true,
        masterEntityFqcn = "com.example.OrderEntity",  // fully-qualified master class name
        masterKeyAttribute = "orderId",                 // attribute on THIS entity holding master's ID
        onMasterDelete = QfMasterRelation.OnMasterDelete.CASCADE_DELETE
    )
)
public class OrderItemEntity {

    private String orderId;   // master's ID

    @QfI18n(defaultMessage = "Product Name")
    @QfListAttribute
    @QfCreateAttribute(requiredOn = @QfRequiredOn(always = true))
    private String productName;

    @QfI18n(defaultMessage = "Quantity")
    @QfListAttribute
    @QfCreateAttribute(requiredOn = @QfRequiredOn(always = true))
    private Integer quantity;
}

onMasterDelete options:

  • CASCADE_DELETE — delete detail records when master is deleted
  • RESTRICT — reject master deletion if details exist
  • IGNORE — leave detail records orphaned

Enabling Change History

java
@QfEntity(
    name = @QfI18n(defaultMessage = "Product", texts = {}),
    autoHistoryEnabled = true    // enable automatic change history
)
public class ProductEntity {
    // ...
}

When autoHistoryEnabled = true:

  • All update and delete operations are automatically recorded
  • A history query endpoint is automatically generated

Tree Structure Entities

Configure tree behavior via treePolicy:

java
@QfEntity(
    name = @QfI18n(defaultMessage = "Category", texts = {}),
    treePolicy = @QfTreePolicy
)
public class CategoryEntity {

    @QfParent
    private String parentId;   // parent node ID

    @QfTreeDepth
    private Integer depth;     // depth level

    @QfI18n(defaultMessage = "Category Name")
    @QfListAttribute
    private String name;
}

With tree policy configured, a tree query endpoint is automatically generated.


Full Example: Composite Entity

java
@QfEntity(
    name = @QfI18n(
        defaultMessage = "Employee",
        texts = { @QfI18nText(locale = "ko", message = "직원") }
    ),
    ownerPolicy = @QfOwnerPolicy(
        enabled = true,
        attribute = "orgId"
    ),
    autoHistoryEnabled = true,
    deletePolicy = @QfCrudPolicy(enabled = true),
    capabilityKey = "hr-management"
)
public class EmployeeEntity {

    // org scope — filtered automatically
    private String orgId;

    // employee number
    @QfI18n(defaultMessage = "Employee Number")
    @QfListAttribute(sortable = true)
    @QfDetailAttribute
    @QfCreateAttribute(requiredOn = @QfRequiredOn(always = true))
    @QfSearch(type = QfSearch.Type.text)
    private String employeeNumber;

    // full name
    @QfI18n(defaultMessage = "Full Name")
    @QfListAttribute(sortable = true)
    @QfDetailAttribute
    @QfCreateAttribute(requiredOn = @QfRequiredOn(always = true))
    @QfUpdateAttribute(requiredOn = @QfRequiredOn(always = true))
    @QfSearch(type = QfSearch.Type.text)
    private String fullName;

    // SSN — encrypted, no search
    @QfI18n(defaultMessage = "SSN")
    @QfDetailAttribute
    @QfCreateAttribute
    @QfCrypto
    private String ssn;

    // phone — encrypted with regex validation
    @QfI18n(defaultMessage = "Phone")
    @QfDetailAttribute
    @QfCreateAttribute
    @QfUpdateAttribute
    @QfCrypto
    @QfValidationRule(
        rule = QfValidationRule.Rule.regex,
        params = "^(?:\\+82[-\\s]?)?(?:0?1[0-9])[-\\s]?\\d{3,4}[-\\s]?\\d{4}$"
    )
    private String phone;

    // rank — from code group
    @QfI18n(defaultMessage = "Rank")
    @QfListAttribute
    @QfDetailAttribute
    @QfCreateAttribute
    @QfUpdateAttribute
    @QfCodeGroup(code = "EMPLOYEE_RANK")
    private String rank;

    // join date
    @QfI18n(defaultMessage = "Join Date")
    @QfListAttribute
    @QfDetailAttribute
    @QfCreateAttribute
    private LocalDate joinDate;
}

Next Steps

Released under the Apache 2.0 License.