DEV Community

David Lastrucci
David Lastrucci

Posted on

Entity mapping in depth: attributes, types, and nullable fields

In the previous article we created a simple entity and performed CRUD operations. Now let's look at how entity mapping actually works under the hood, and how to handle more realistic scenarios like nullable fields and optimistic locking.

The mapping attributes

Every Trysil entity needs at least two class-level attributes:

[TTable('Contacts')]      // which table
[TSequence('ContactsID')]   // how the PK is generated
TTContact = class
Enter fullscreen mode Exit fullscreen mode

And every mapped field needs a [TColumn] attribute:

[TColumn('Firstname')]
FFirstname: String;
Enter fullscreen mode Exit fullscreen mode

Here is the full set of mapping attributes:

Attribute Level Purpose
TTable Class Maps the class to a database table
TSequence Class Names the sequence for the PK
TPrimaryKey Field Marks the primary key field
TColumn Field Maps a field to a column
TVersionColumn Field Enables optimistic locking
TRelation Class Declares a parent-child relationship (covered in a later article)

Core types

Trysil defines three foundational types in Trysil.Types:

TTPrimaryKey

[TPrimaryKey]
[TColumn('ID')]
FID: TTPrimaryKey;
Enter fullscreen mode Exit fullscreen mode

TTPrimaryKey is an alias for Int32. Every entity must have exactly one primary key field of this type. The value is auto-generated by the database.

The ID property is read-only — you never set it manually:

property ID: TTPrimaryKey read FID;
Enter fullscreen mode Exit fullscreen mode

TTVersion

[TColumn('VersionID')]
[TVersionColumn]
FVersionID: TTVersion;
Enter fullscreen mode Exit fullscreen mode

TTVersion is also an Int32. When you mark a field with [TVersionColumn], Trysil includes it in the WHERE clause of UPDATE and DELETE statements:

UPDATE Contacts SET ... WHERE ID = :ID AND VersionID = :VersionID
Enter fullscreen mode Exit fullscreen mode

If the version in the database does not match the version in your object, the update affects zero rows and Trysil raises an exception. This is optimistic locking — it prevents two users from silently overwriting each other's changes.

After a successful update, Trysil increments the version automatically.

TTNullable<T>

Database columns are often nullable, but Delphi value types (Integer, TDateTime, Double) cannot be null. Trysil solves this with a generic record:

[TColumn('BirthDate')]
FBirthDate: TTNullable<TDateTime>;

[TColumn('Score')]
FScore: TTNullable<Integer>;
Enter fullscreen mode Exit fullscreen mode

TTNullable<T> has no default constructor. When you declare a field of this type and do not assign a value, it is null:

var
  LDate: TTNullable<TDateTime>;
begin
  // LDate is null here

  if LDate.IsNull then
    WriteLn('No date set');

  // Assign a value
  LDate := TTNullable<TDateTime>.Create(Now);
  WriteLn(DateTimeToStr(LDate.Value));
end;
Enter fullscreen mode Exit fullscreen mode

In the database, a null TTNullable<T> field is stored as SQL NULL. When reading, SQL NULL maps back to the null state.

A more realistic entity

Let's build a TTProduct entity that uses all of these types:

unit Product.Model;

{$WARN UNKNOWN_CUSTOM_ATTRIBUTE ERROR}

interface

uses
  System.SysUtils,
  Trysil.Types,
  Trysil.Attributes,
  Trysil.Validation.Attributes;

type
  [TTable('Products')]
  [TSequence('ProductsID')]
  TTProduct = class
  strict private
    [TPrimaryKey]
    [TColumn('ID')]
    FID: TTPrimaryKey;

    [TRequired]
    [TMaxLength(100)]
    [TColumn('Description')]
    FDescription: String;

    [TGreater(0.00)]
    [TColumn('Price')]
    FPrice: Double;

    [TColumn('DiscountedPrice')]
    FDiscountedPrice: TTNullable<Double>;

    [TColumn('AvailableFrom')]
    FAvailableFrom: TTNullable<TDateTime>;

    [TColumn('VersionID')]
    [TVersionColumn]
    FVersionID: TTVersion;
  public
    property ID: TTPrimaryKey read FID;
    property Description: String read FDescription write FDescription;
    property Price: Double read FPrice write FPrice;
    property DiscountedPrice: TTNullable<Double>
      read FDiscountedPrice write FDiscountedPrice;
    property AvailableFrom: TTNullable<TDateTime>
      read FAvailableFrom write FAvailableFrom;
    property VersionID: TTVersion read FVersionID;
  end;
Enter fullscreen mode Exit fullscreen mode

The matching SQL table:

CREATE TABLE Products (
  ID INTEGER PRIMARY KEY,
  Description TEXT NOT NULL,
  Price REAL NOT NULL,
  DiscountedPrice REAL,
  AvailableFrom TEXT,
  VersionID INTEGER NOT NULL DEFAULT 1
);
Enter fullscreen mode Exit fullscreen mode

Note how DiscountedPrice and AvailableFrom are nullable in both the Delphi entity and the SQL schema.

Working with nullable fields

var
  LProduct: TTProduct;
begin
  LProduct := LContext.CreateEntity<TTProduct>();
  LProduct.Description := 'Mechanical keyboard';
  LProduct.Price := 149.99;
  // DiscountedPrice and AvailableFrom are null — we simply don't set them

  LContext.Insert<TTProduct>(LProduct);

  // Later, set a discounted price
  LProduct.DiscountedPrice := TTNullable<Double>.Create(119.99);
  LContext.Update<TTProduct>(LProduct);
end;
Enter fullscreen mode Exit fullscreen mode

How the mapping cache works

The first time you use an entity type with TTContext, Trysil reads its attributes via RTTI and builds a TTTableMap — an internal representation of the table name, columns, primary key, version column, and relationships.

This mapping is cached globally in TTMapper.Instance (a singleton). Subsequent operations on the same entity type skip the RTTI scan entirely. This means the reflection cost is paid only once per entity type for the lifetime of your application.

Compile-time safety

Always add this directive at the top of your model units:

{$WARN UNKNOWN_CUSTOM_ATTRIBUTE ERROR}
Enter fullscreen mode Exit fullscreen mode

Without it, a typo like [TColum('Name')] compiles silently and the field is never mapped. With the directive, the compiler flags it as an error immediately.

What is next

We now know how to map classes to tables, handle nullable fields, and rely on optimistic locking for safe concurrent updates. In the next article we will explore validation — how Trysil checks your data before it hits the database.


Trysil is open-source and available on GitHub. Feedback and contributions are welcome!

Top comments (0)