DEV Community

David Lastrucci
David Lastrucci

Posted on

Meet Trysil: a lightweight ORM for Delphi

If you have ever written a Delphi application that talks to a database, you know the routine: write SQL by hand, manage parameters, loop through datasets, and copy values into objects field by field. It works, but it is tedious and error-prone.

Trysil is an open-source ORM for Delphi that eliminates that boilerplate. You decorate your classes with attributes, and the framework handles the rest — mapping, querying, inserting, updating, deleting. It is lightweight, attribute-driven, and built on top of FireDAC, so it works with SQLite, PostgreSQL, SQL Server, and Firebird out of the box.

In this first article we will go from zero to a working CRUD application with SQLite.

Installation

You can install Trysil in three ways:

  • GetIt — search for "Trysil" in the Embarcadero GetIt Package Manager
  • Bossboss install davidlastrucci/Trysil
  • Manual — clone the GitHub repo, open Packages/<ver>/Trysil.groupproj, and build all

After installation, point your project's Search Path to the compiled output directory.

Your first entity

An entity in Trysil is a plain Delphi class with attributes. No base class to inherit, no interface to implement.

unit Contact.Model;

{$WARN UNKNOWN_CUSTOM_ATTRIBUTE ERROR}

interface

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

type
  [TTable('Contacts')]
  [TSequence('ContactsID')]
  TTContact = class
  strict private
    [TPrimaryKey]
    [TColumn('ID')]
    FID: TTPrimaryKey;

    [TRequired]
    [TMaxLength(50)]
    [TColumn('Firstname')]
    FFirstname: String;

    [TRequired]
    [TMaxLength(50)]
    [TColumn('Lastname')]
    FLastname: String;

    [TMaxLength(255)]
    [TEmail]
    [TColumn('Email')]
    FEmail: String;

    [TColumn('VersionID')]
    [TVersionColumn]
    FVersionID: TTVersion;
  public
    property ID: TTPrimaryKey read FID;
    property Firstname: String read FFirstname write FFirstname;
    property Lastname: String read FLastname write FLastname;
    property Email: String read FEmail write FEmail;
    property VersionID: TTVersion read FVersionID;
  end;
Enter fullscreen mode Exit fullscreen mode

A few things to note:

  • [TTable('Contacts')] maps the class to the Contacts table.
  • [TSequence('ContactsID')] tells Trysil how the primary key is generated.
  • [TPrimaryKey] and [TColumn('...')] mark the primary key and map each field to a column.
  • [TVersionColumn] enables optimistic locking — Trysil will check this value on every update to prevent lost writes.
  • [TRequired], [TMaxLength], [TEmail] are validation attributes — we will cover them in depth in a later article.
  • {$WARN UNKNOWN_CUSTOM_ATTRIBUTE ERROR} turns typos in attribute names into compile-time errors. Always add this.

Fields are strict private with public properties. The ORM accesses them through RTTI.

Connecting to SQLite

uses
  Trysil.Data,
  Trysil.Data.FireDAC.SQLite,
  Trysil.Data.FireDAC.ConnectionPool,
  Trysil.Context;

var
  LConnection: TTConnection;
  LContext: TTContext;
begin
  // Disable connection pooling (not needed for desktop apps)
  TTFireDACConnectionPool.Instance.Config.Enabled := False;

  // Register and create the connection
  TTSQLiteConnection.RegisterConnection('MyApp', 'contacts.db');
  LConnection := TTSQLiteConnection.Create('MyApp');
  try
    LContext := TTContext.Create(LConnection);
    try
      // ... use the context
    finally
      LContext.Free;
    end;
  finally
    LConnection.Free;
  end;
end;
Enter fullscreen mode Exit fullscreen mode

TTContext is the single entry point for all ORM operations. You create it with a connection, and it gives you methods for reading, writing, and querying entities.

CRUD operations

Create

var
  LContact: TTContact;
begin
  LContact := LContext.CreateEntity<TTContact>();
  LContact.Firstname := 'Ada';
  LContact.Lastname := 'Lovelace';
  LContact.Email := 'ada@example.com';
  LContext.Insert<TTContact>(LContact);
end;
Enter fullscreen mode Exit fullscreen mode

Always use CreateEntity<T> to create new entities — it initializes internal state that the context needs to track the object.

Read

var
  LContacts: TTList<TTContact>;
begin
  LContacts := TTList<TTContact>.Create;
  try
    // Load all contacts
    LContext.SelectAll<TTContact>(LContacts);

    for LContact in LContacts do
      WriteLn(Format('%s %s — %s', [
        LContact.Firstname,
        LContact.Lastname,
        LContact.Email]));
  finally
    LContacts.Free;
  end;
end;
Enter fullscreen mode Exit fullscreen mode

To fetch a single entity by ID:

var
  LContact: TTContact;
begin
  LContact := LContext.Get<TTContact>(42);
  // raises an exception if not found
end;
Enter fullscreen mode Exit fullscreen mode

Update

LContact.Email := 'ada.lovelace@example.com';
LContext.Update<TTContact>(LContact);
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, Trysil generates an UPDATE statement that includes the VersionID in the WHERE clause. If another user has modified the same record in the meantime, the update fails with a concurrency error rather than silently overwriting their changes.

Delete

LContext.Delete<TTContact>(LContact);
Enter fullscreen mode Exit fullscreen mode

The SQL table

For completeness, here is the SQLite schema that matches our entity:

CREATE TABLE Contacts (
  ID INTEGER PRIMARY KEY,
  Firstname TEXT NOT NULL,
  Lastname TEXT NOT NULL,
  Email TEXT,
  VersionID INTEGER NOT NULL DEFAULT 1
);
Enter fullscreen mode Exit fullscreen mode

What is next

In this article we defined an entity, connected to SQLite, and performed full CRUD — all without writing a single line of SQL.

In the next article we will look deeper into entity mapping: nullable fields, custom types, and how Trysil handles the relationship between your Delphi classes and your database schema.

The full source code is available on GitHub.


Trysil is an open-source project. If you find it useful, consider giving it a star on GitHub!

Top comments (0)