In most business applications, you need to answer questions like: Who created this record? When was it last modified? Can we undo this deletion?
Implementing this by hand means adding columns, writing triggers or hooks, and remembering to update them on every operation. Trysil does it with six attributes.
The change tracking attributes
| Attribute | Set on | Required field type |
|---|---|---|
[TCreatedAt] |
Insert | TTNullable<TDateTime> |
[TCreatedBy] |
Insert | String |
[TUpdatedAt] |
Update | TTNullable<TDateTime> |
[TUpdatedBy] |
Update | String |
[TDeletedAt] |
Delete | TTNullable<TDateTime> |
[TDeletedBy] |
Delete | String |
You add them to your entity fields, and Trysil fills them in automatically.
Adding an audit trail
unit Article.Model;
{$WARN UNKNOWN_CUSTOM_ATTRIBUTE ERROR}
interface
uses
Trysil.Types,
Trysil.Attributes,
Trysil.Validation.Attributes;
type
[TTable('Articles')]
[TSequence('ArticlesID')]
TTArticle = class
strict private
[TPrimaryKey]
[TColumn('ID')]
FID: TTPrimaryKey;
[TRequired]
[TMaxLength(200)]
[TColumn('Title')]
FTitle: String;
[TColumn('Body')]
FBody: String;
[TCreatedAt]
[TColumn('CreatedAt')]
FCreatedAt: TTNullable<TDateTime>;
[TCreatedBy]
[TColumn('CreatedBy')]
FCreatedBy: String;
[TUpdatedAt]
[TColumn('UpdatedAt')]
FUpdatedAt: TTNullable<TDateTime>;
[TUpdatedBy]
[TColumn('UpdatedBy')]
FUpdatedBy: String;
[TColumn('VersionID')]
[TVersionColumn]
FVersionID: TTVersion;
public
property ID: TTPrimaryKey read FID;
property Title: String read FTitle write FTitle;
property Body: String read FBody write FBody;
property CreatedAt: TTNullable<TDateTime> read FCreatedAt;
property CreatedBy: String read FCreatedBy;
property UpdatedAt: TTNullable<TDateTime> read FUpdatedAt;
property UpdatedBy: String read FUpdatedBy;
end;
Now when you insert or update:
var
LArticle: TTArticle;
begin
LArticle := LContext.CreateEntity<TTArticle>();
LArticle.Title := 'Getting started with Trysil';
LArticle.Body := 'In this article...';
LContext.Insert<TTArticle>(LArticle);
// CreatedAt is now set to the current timestamp
// CreatedBy is set to the current user name
end;
LArticle.Title := 'Getting started with Trysil (updated)';
LContext.Update<TTArticle>(LArticle);
// UpdatedAt is now set to the current timestamp
// UpdatedBy is set to the current user name
// CreatedAt and CreatedBy remain unchanged
Providing the current user
The *By fields need to know who the current user is. You provide this via a callback on TTContext:
LContext.OnGetCurrentUser := function: String
begin
result := 'david.lastrucci';
end;
In a real application, this might read from an authentication token, a session variable, or a thread-local user context. If you do not assign the callback, Trysil writes an empty string.
Soft delete
Traditional DELETE removes the row from the database. In many scenarios — audit compliance, undo functionality, data recovery — you want to keep the record but mark it as deleted.
Trysil supports this natively. Add [TDeletedAt] (and optionally [TDeletedBy]) to your entity:
[TTable('Articles')]
[TSequence('ArticlesID')]
TTArticle = class
strict private
// ... other fields ...
[TDeletedAt]
[TColumn('DeletedAt')]
FDeletedAt: TTNullable<TDateTime>;
[TDeletedBy]
[TColumn('DeletedBy')]
FDeletedBy: String;
[TColumn('VersionID')]
[TVersionColumn]
FVersionID: TTVersion;
public
// ... properties ...
property DeletedAt: TTNullable<TDateTime> read FDeletedAt;
property DeletedBy: String read FDeletedBy;
end;
What changes
When an entity has a [TDeletedAt] field, calling Delete<T> no longer executes a SQL DELETE. Instead it executes:
UPDATE Articles
SET DeletedAt = :DeletedAt, DeletedBy = :DeletedBy, VersionID = VersionID + 1
WHERE ID = :ID AND VersionID = :VersionID
The record stays in the database, but it is marked as deleted.
Automatic exclusion
All SELECT queries on that entity automatically add DeletedAt IS NULL to the WHERE clause. Soft-deleted records are invisible by default — your application code does not need to change at all.
// This only returns non-deleted articles
LContext.SelectAll<TTArticle>(LArticles);
Including deleted records
Sometimes you need to see deleted records (admin panels, audit logs). Use the filter builder:
var
LBuilder: TTFilterBuilder<TTArticle>;
LFilter: TTFilter;
begin
LBuilder := LContext.CreateFilterBuilder<TTArticle>();
try
LFilter := LBuilder
.IncludeDeleted
.OrderByDesc('DeletedAt')
.Build;
LContext.Select<TTArticle>(LAllArticles, LFilter);
finally
LBuilder.Free;
end;
end;
Relation checks and soft delete
When you soft-delete a parent record, Trysil skips the child relation check. This makes sense: the record is not being physically removed, so foreign key integrity is preserved.
Putting it all together
Here is the SQL table that supports full change tracking with soft delete:
CREATE TABLE Articles (
ID INTEGER PRIMARY KEY,
Title TEXT NOT NULL,
Body TEXT,
CreatedAt TEXT,
CreatedBy TEXT,
UpdatedAt TEXT,
UpdatedBy TEXT,
DeletedAt TEXT,
DeletedBy TEXT,
VersionID INTEGER NOT NULL DEFAULT 1
);
And the complete entity lifecycle:
var
LArticle: TTArticle;
begin
// Create
LArticle := LContext.CreateEntity<TTArticle>();
LArticle.Title := 'My article';
LArticle.Body := 'Content here';
LContext.Insert<TTArticle>(LArticle);
// CreatedAt = 2026-04-09 10:30:00, CreatedBy = 'david.lastrucci'
// Update
LArticle.Title := 'My article (revised)';
LContext.Update<TTArticle>(LArticle);
// UpdatedAt = 2026-04-09 11:15:00, UpdatedBy = 'david.lastrucci'
// Soft delete
LContext.Delete<TTArticle>(LArticle);
// DeletedAt = 2026-04-09 14:00:00, DeletedBy = 'david.lastrucci'
// Record is still in the database, but invisible to normal queries
end;
Series recap
Over these six articles we have covered the core of Trysil:
- First contact — entity, connection, CRUD
- Entity mapping — attributes, types, nullable fields, optimistic locking
- Validation — declarative rules, custom validators, error handling
- Filtering — fluent query builder, sorting, pagination
- Relations — lazy loading, cascade delete, parent-child patterns
- Change tracking — audit trails, soft delete, automatic exclusion
Trysil also includes a JSON serialization module, an HTTP/REST hosting module with attribute-based routing and JWT authentication, and a Unit of Work pattern via TTSession<T>. These are topics for future articles.
If you want to explore further, the GitHub repo contains full demo projects, a cookbook with 17 copy-paste recipes, and complete API documentation.
Trysil is open-source and available on GitHub. If this series helped you, consider giving the project a star — it helps other Delphi developers discover it!
Top comments (0)