Timeline Model
A timeline model records historical slices of data over time. It is useful for business data that depends on an effective date (for example, department structures or reports that change before/after a specific date). A business record id can have multiple slices; each slice is identified by sliceId, and effectiveStartDate/effectiveEndDate define the effective range.
1. Timeline Model Metadata
1.1 Timeline Attribute at Model Level
timeline = trueindicates this is a timeline model. It must contain the reserved fieldseffectiveStartDateandeffectiveEndDate. The system validates these fields on startup and throws an exception if missing.timeline = falseindicates a non-timeline model. Non-timeline models must not define the reserved fieldseffectiveStartDateandeffectiveEndDate.
1.2 Primary Keys and Fields
sliceId: physical primary key of a timeline model, used to update a slice.effectiveStartDate: effective start date of the timeline data.effectiveEndDate: effective end date of the timeline data.id: logical (business) primary key, compatible with non-timeline models. All business foreign keys referencing a timeline model use this field.- If your database needs an auto-increment record number (such as
record_id) for change logs, you can add it yourself. It is not a framework-reserved field. - Recommended unique constraint:
(id, effectiveStartDate, effectiveEndDate).
1.3 Metadata Relationships
- Timeline models can relate to themselves via One2One, Many2One, One2Many, Many2Many. Storage and references use the logical primary key
id. - When a timeline model relates to a non-timeline model, relation tables store the timeline model logical key
id. - When a non-timeline model relates to a timeline model, Many2One/One2One fields and Many2Many join tables store the timeline model logical key
id. - Association reads use
effectiveDateby default (current date if not specified), so there may be no effective slice for the current date. - In cascade query chains (for example, timeline -> non-timeline -> timeline),
effectiveDateshould be propagated to the last model to keep consistency.
1.4 Cascaded Fields
- Cascaded fields are based on Many2One/One2One associations. When the related model is a timeline model,
Context.effectiveDateis used to query the related data.
1.5 Timeline Data Concepts
- Every slice must have
effectiveStartDateandeffectiveEndDate, and slices for the sameidare expected to be continuous and non-overlapping. - To simplify queries, the last slice typically uses
effectiveEndDate = 9999-12-31. - In most cases, you only need to set
effectiveStartDate; the system computes and fillseffectiveEndDatebased on adjacent slices. - Physical record: each slice is a physical record (identified by
sliceId). Any change in effective dates creates or updates physical slices. Change logs are bound to physical records. - Logical record: a group of physical slices that share the same logical
id. Business foreign keys reference the logicalid, and association reads return the slice effective on the requested date.
Example timeline slices (same logical department id):
| sliceId (physical) | id (logical) | Department Code | Department Name | effectiveStartDate | effectiveEndDate | Manager | | --- | --- | --- | --- | --- | --- | | | 3 | 6 | D001 | Product R&D Dept | 2022-09-01 | 9999-12-31 | Joan | | 2 | 6 | D001 | R&D Dept | 2020-05-11 | 2022-08-31 | Tom | | 1 | 6 | D001 | R&D Dept | 2019-08-01 | 2020-05-10 | Mars |
2. Common Scenarios
2.1 Effective Date Propagation
effectiveDateis aLocalDatestored inContext, defaulting to the current date.- Query data effective on a specific date:
effectiveStartDate <= effectiveDate && effectiveEndDate >= effectiveDate - Query data effective within a period (startDateValue, endDateValue must be non-null):
effectiveStartDate <= endDateValue && effectiveEndDate >= startDateValue - To query all slices for a business record, use
acrossTimelineData()withidfilters (or includeeffectiveStartDate/effectiveEndDatein filters). - Typical adjacent slice lookups:
previous: id = {id} AND effective_end_date = {effectiveStartDate - 1}next: id = {id} AND effective_start_date = {effectiveEndDate + 1}
2.2 read/search APIs
- Queries like
getById/getByIds/searchList/searchPagereturn only slices effective oneffectiveDateby default. - To query history across time, use
FlexQuery#acrossTimelineData()or includeeffectiveStartDate/effectiveEndDatein filters. - Cascaded reads propagate
effectiveDate.
2.3 create APIs
- For
createOne/createList, ifeffectiveStartDateis empty, it uses the currenteffectiveDate; ifeffectiveEndDateis empty, it is set to9999-12-31. - If an existing
idis provided, the system automatically splits or adjusts adjacent slices based on the neweffectiveStartDate.
2.4 update APIs
- The current implementation uses
sliceIdas the update primary key. UpdatingeffectiveStartDateautomatically corrects adjacent slices’effectiveEndDate. - Manual updates to
effectiveEndDateare not recommended. To create a new slice, usecreatewith an existingidand a neweffectiveStartDate. - If an upper layer provides a “correct”-style API (update data without creating a new slice), it should locate by
sliceId(the ORM currently does not provide a dedicated correct API).
2.5 delete APIs
deleteById/deleteByIds: deletes all slices for a businessid.deleteBySliceId: deletes a single slice and automatically corrects adjacent slice ranges.
2.6 search Join Rules for Timeline Associations
- When the related object is a timeline model, Many2One/One2One queries automatically append to the
LEFT JOIN ONclause:effectiveStartDate <= effectiveDate AND effectiveEndDate >= effectiveDate. - One2Many/Many2Many cascades also filter slices based on
effectiveDate.
Examples
1) Model Definition
@Data
@Schema(name = "ProductPrice")
@EqualsAndHashCode(callSuper = true)
public class ProductPrice extends TimelineModel {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "Business ID")
private Long id;
@Schema(description = "Product ID")
private Long productId;
@Schema(description = "Price")
private BigDecimal price;
}2) Query Current and Historical Slices
ContextHolder.getContext().setEffectiveDate(LocalDate.of(2025, 1, 1));
Filters filters = new Filters().eq("productId", 1001L);
List<Map<String, Object>> current = modelService.searchList("ProductPrice", new FlexQuery(filters));
FlexQuery historyQuery = new FlexQuery(new Filters().eq("id", 1L))
.acrossTimelineData()
.orderBy(Orders.ofAsc("effectiveStartDate"));
List<Map<String, Object>> history = modelService.searchList("ProductPrice", historyQuery);3. Performance
- By default, queries do not scan across time (no
effectiveStartDate/effectiveEndDatefilters and noacrossTimelineData()), which reduces scanning. - Add indexes for
effectiveStartDateandeffectiveEndDate.
4. Time-Effective (Non-Timeline) Data
Some models need history records with effective dates but are not timeline models (for example, HR changes, work history, education history). These cases may allow multiple records on the same day and do not require continuous slices.
In Softa, timeline fields are reserved. If you need history-only behavior, use a separate history model or different field names, and keep timeline = false to avoid timeline slice semantics.