<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged delphi</title>
<link>https://coderlegion.com/tag/delphi</link>
<description>Powered by Question2Answer</description>
<item>
<title>Filtering, sorting, and pagination with the fluent query builder</title>
<link>https://coderlegion.com/15299/filtering-sorting-and-pagination-with-the-fluent-query-builder</link>
<description>&lt;p&gt;So far in this series we have mapped entities, validated them, and performed basic CRUD. But real applications rarely load &lt;em&gt;all&lt;/em&gt; records from a table. You need to search, filter, sort, and paginate.&lt;/p&gt;
&lt;p&gt;Trysil provides a fluent API for this: &lt;strong&gt;&lt;code&gt;TTFilterBuilder&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt;. You chain method calls to build a filter, then pass it to &lt;code&gt;Select&amp;lt;T&amp;gt;&lt;/code&gt;. The builder generates parameterized SQL behind the scenes — no string concatenation, no injection risk.&lt;/p&gt;
&lt;h3 id=&quot;basic-filtering&quot;&gt;Basic filtering&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;uses
  Trysil.Filter;

var
  LBuilder: TTFilterBuilder&amp;lt;TTContact&amp;gt;;
  LFilter: TTFilter;
  LContacts: TTList&amp;lt;TTContact&amp;gt;;
begin
  LContacts := TTList&amp;lt;TTContact&amp;gt;.Create;
  try
    LBuilder := LContext.CreateFilterBuilder&amp;lt;TTContact&amp;gt;();
    try
      LFilter := LBuilder
        .Where('Lastname').Equal('Smith')
        .Build;

      LContext.Select&amp;lt;TTContact&amp;gt;(LContacts, LFilter);
    finally
      LBuilder.Free;
    end;

    for LContact in LContacts do
      WriteLn(Format('%s %s', [LContact.Firstname, LContact.Lastname]));
  finally
    LContacts.Free;
  end;
end;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The flow is always the same:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a builder with &lt;code&gt;LContext.CreateFilterBuilder&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Chain conditions with &lt;code&gt;.Where&lt;/code&gt;, &lt;code&gt;.AndWhere&lt;/code&gt;, &lt;code&gt;.OrWhere&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Apply an operator (&lt;code&gt;.Equal&lt;/code&gt;, &lt;code&gt;.Like&lt;/code&gt;, &lt;code&gt;.Greater&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;.Build&lt;/code&gt; to get a &lt;code&gt;TTFilter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Pass the filter to &lt;code&gt;LContext.Select&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;available-operators&quot;&gt;Available operators&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Method &lt;/th&gt;&lt;th&gt;SQL&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.Equal(value)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;= :param&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.NotEqual(value)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;lt;&amp;gt; :param&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.Greater(value)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;gt; :param&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.GreaterOrEqual(value)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;gt;= :param&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.Less(value)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;lt; :param&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.LessOrEqual(value)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;lt;= :param&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.Like(pattern)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;LIKE :param&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.NotLike(pattern)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;NOT LIKE :param&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.IsNull&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;IS NULL&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.IsNotNull&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;IS NOT NULL&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;combining-conditions&quot;&gt;Combining conditions&lt;/h3&gt;
&lt;p&gt;Use &lt;code&gt;.AndWhere&lt;/code&gt; and &lt;code&gt;.OrWhere&lt;/code&gt; to combine multiple conditions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;LFilter := LBuilder
  .Where('Lastname').Equal('Smith')
  .AndWhere('Email').IsNotNull
  .Build;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;LFilter := LBuilder
  .Where('City').Equal('Rome')
  .OrWhere('City').Equal('Milan')
  .Build;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;pattern-matching-with-like&quot;&gt;Pattern matching with LIKE&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;LFilter := LBuilder
  .Where('Lastname').Like('Sm%')
  .Build;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Standard SQL wildcards apply: &lt;code&gt;%&lt;/code&gt; matches any sequence of characters, &lt;code&gt;_&lt;/code&gt; matches a single character.&lt;/p&gt;
&lt;h3 id=&quot;sorting&quot;&gt;Sorting&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;LFilter := LBuilder
  .Where('Country').Equal('Italy')
  .OrderByAsc('Lastname')
  .Build;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can chain multiple sort clauses:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;LFilter := LBuilder
  .Where('Country').Equal('Italy')
  .OrderByAsc('Lastname')
  .OrderByAsc('Firstname')
  .Build;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For descending order, use &lt;code&gt;.OrderByDesc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;LFilter := LBuilder
  .OrderByDesc('Price')
  .Build;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: you can sort without filtering — just skip the &lt;code&gt;.Where&lt;/code&gt; call.&lt;/p&gt;
&lt;h3 id=&quot;pagination&quot;&gt;Pagination&lt;/h3&gt;
&lt;p&gt;For large datasets, load data in pages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;const
  PageSize = 20;
var
  LPage: Integer;
begin
  LPage := 3; // zero-based page index

  LFilter := LBuilder
    .OrderByAsc('Lastname')
    .Limit(PageSize)
    .Offset(LPage * PageSize)
    .Build;

  LContext.Select&amp;lt;TTContact&amp;gt;(LContacts, LFilter);
end;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.Limit(n)&lt;/code&gt; sets the maximum number of rows to return. &lt;code&gt;.Offset(n)&lt;/code&gt; skips the first n rows. Combined with sorting, this gives you clean, predictable pagination.&lt;/p&gt;
&lt;h3 id=&quot;counting-records&quot;&gt;Counting records&lt;/h3&gt;
&lt;p&gt;Sometimes you need the total count (for example, to display &quot;Page 3 of 12&quot;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;var
  LCount: Integer;
begin
  LFilter := LBuilder
    .Where('Country').Equal('Italy')
    .Build;

  LCount := LContext.SelectCount&amp;lt;TTContact&amp;gt;(LFilter);
end;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SelectCount&amp;lt;T&amp;gt;&lt;/code&gt; returns the number of matching rows without loading the entities into memory.&lt;/p&gt;
&lt;h3 id=&quot;a-complete-example&quot;&gt;A complete example&lt;/h3&gt;
&lt;p&gt;Here is a realistic search function that combines filtering, sorting, pagination, and counting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;procedure TContactService.Search(
  const ASearchText: String;
  const APage: Integer;
  const APageSize: Integer;
  const AContacts: TTList&amp;lt;TTContact&amp;gt;;
  out ATotalCount: Integer);
var
  LBuilder: TTFilterBuilder&amp;lt;TTContact&amp;gt;;
  LFilter: TTFilter;
begin
  LBuilder := FContext.CreateFilterBuilder&amp;lt;TTContact&amp;gt;();
  try
    if not ASearchText.IsEmpty then
      LBuilder
        .Where('Lastname').Like(Format('%s%%', [ASearchText]))
        .OrWhere('Firstname').Like(Format('%s%%', [ASearchText]));

    LBuilder
      .OrderByAsc('Lastname')
      .OrderByAsc('Firstname');

    LFilter := LBuilder.Build;
    ATotalCount := FContext.SelectCount&amp;lt;TTContact&amp;gt;(LFilter);

    LBuilder
      .Limit(APageSize)
      .Offset(APage * APageSize);

    LFilter := LBuilder.Build;
    FContext.Select&amp;lt;TTContact&amp;gt;(AContacts, LFilter);
  finally
    LBuilder.Free;
  end;
end;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-is-next&quot;&gt;What is next&lt;/h3&gt;
&lt;p&gt;We can now search, sort, and paginate our data with a clean fluent API. In the next article we will tackle &lt;strong&gt;relations and lazy loading&lt;/strong&gt; — how to model parent-child relationships and load related entities on demand.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Trysil is open-source and available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/davidlastrucci/Trysil&quot;&gt;GitHub&lt;/a&gt;. Stars and feedback are always appreciated!&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15299/filtering-sorting-and-pagination-with-the-fluent-query-builder</guid>
<pubDate>Mon, 20 Apr 2026 12:51:41 +0000</pubDate>
</item>
<item>
<title>Meet Trysil: a lightweight ORM for Delphi</title>
<link>https://coderlegion.com/15298/meet-trysil-a-lightweight-orm-for-delphi</link>
<description>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trysil&lt;/strong&gt; 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.&lt;/p&gt;
&lt;p&gt;In this first article we will go from zero to a working CRUD application with SQLite.&lt;/p&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;p&gt;You can install Trysil in three ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GetIt&lt;/strong&gt; — search for &quot;Trysil&quot; in the Embarcadero GetIt Package Manager&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Boss&lt;/strong&gt; — &lt;code&gt;boss install davidlastrucci/Trysil&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manual&lt;/strong&gt; — clone the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/davidlastrucci/Trysil&quot;&gt;GitHub repo&lt;/a&gt;, open &lt;code&gt;Packages/&amp;lt;ver&amp;gt;/Trysil.groupproj&lt;/code&gt;, and build all&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After installation, point your project's Search Path to the compiled output directory.&lt;/p&gt;
&lt;h3 id=&quot;your-first-entity&quot;&gt;Your first entity&lt;/h3&gt;
&lt;p&gt;An entity in Trysil is a plain Delphi class with attributes. No base class to inherit, no interface to implement.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;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;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few things to note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;[TTable('Contacts')]&lt;/code&gt;&lt;/strong&gt; maps the class to the &lt;code&gt;Contacts&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;[TSequence('ContactsID')]&lt;/code&gt;&lt;/strong&gt; tells Trysil how the primary key is generated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;[TPrimaryKey]&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;[TColumn('...')]&lt;/code&gt;&lt;/strong&gt; mark the primary key and map each field to a column.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;[TVersionColumn]&lt;/code&gt;&lt;/strong&gt; enables optimistic locking — Trysil will check this value on every update to prevent lost writes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;[TRequired]&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;[TMaxLength]&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;[TEmail]&lt;/code&gt;&lt;/strong&gt; are validation attributes — we will cover them in depth in a later article.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;{$WARN UNKNOWN_CUSTOM_ATTRIBUTE ERROR}&lt;/code&gt;&lt;/strong&gt; turns typos in attribute names into compile-time errors. Always add this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fields are &lt;code&gt;strict private&lt;/code&gt; with public properties. The ORM accesses them through RTTI.&lt;/p&gt;
&lt;h3 id=&quot;connecting-to-sqlite&quot;&gt;Connecting to SQLite&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;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;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;TTContext&lt;/code&gt; 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.&lt;/p&gt;
&lt;h3 id=&quot;crud-operations&quot;&gt;CRUD operations&lt;/h3&gt;
&lt;h4 id=&quot;create&quot;&gt;Create&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;var
  LContact: TTContact;
begin
  LContact := LContext.CreateEntity&amp;lt;TTContact&amp;gt;();
  LContact.Firstname := 'Ada';
  LContact.Lastname := 'Lovelace';
  LContact.Email := '*Emails are not allowed*';
  LContext.Insert&amp;lt;TTContact&amp;gt;(LContact);
end;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always use &lt;code&gt;CreateEntity&amp;lt;T&amp;gt;&lt;/code&gt; to create new entities — it initializes internal state that the context needs to track the object.&lt;/p&gt;
&lt;h4 id=&quot;read&quot;&gt;Read&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;var
  LContacts: TTList&amp;lt;TTContact&amp;gt;;
begin
  LContacts := TTList&amp;lt;TTContact&amp;gt;.Create;
  try
    // Load all contacts
    LContext.SelectAll&amp;lt;TTContact&amp;gt;(LContacts);

    for LContact in LContacts do
      WriteLn(Format('%s %s — %s', [
        LContact.Firstname,
        LContact.Lastname,
        LContact.Email]));
  finally
    LContacts.Free;
  end;
end;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To fetch a single entity by ID:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;var
  LContact: TTContact;
begin
  LContact := LContext.Get&amp;lt;TTContact&amp;gt;(42);
  // raises an exception if not found
end;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;update&quot;&gt;Update&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;LContact.Email := '*Emails are not allowed*';
LContext.Update&amp;lt;TTContact&amp;gt;(LContact);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Behind the scenes, Trysil generates an UPDATE statement that includes the &lt;code&gt;VersionID&lt;/code&gt; 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.&lt;/p&gt;
&lt;h4 id=&quot;delete&quot;&gt;Delete&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-delphi&quot;&gt;LContext.Delete&amp;lt;TTContact&amp;gt;(LContact);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-sql-table&quot;&gt;The SQL table&lt;/h3&gt;
&lt;p&gt;For completeness, here is the SQLite schema that matches our entity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE Contacts (
  ID INTEGER PRIMARY KEY,
  Firstname TEXT NOT NULL,
  Lastname TEXT NOT NULL,
  Email TEXT,
  VersionID INTEGER NOT NULL DEFAULT 1
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-is-next-1&quot;&gt;What is next&lt;/h3&gt;
&lt;p&gt;In this article we defined an entity, connected to SQLite, and performed full CRUD — all without writing a single line of SQL.&lt;/p&gt;
&lt;p&gt;In the next article we will look deeper into &lt;strong&gt;entity mapping&lt;/strong&gt;: nullable fields, custom types, and how Trysil handles the relationship between your Delphi classes and your database schema.&lt;/p&gt;
&lt;p&gt;The full source code is available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/davidlastrucci/Trysil&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Trysil is an open-source project. If you find it useful, consider giving it a star on GitHub!&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15298/meet-trysil-a-lightweight-orm-for-delphi</guid>
<pubDate>Mon, 20 Apr 2026 12:51:09 +0000</pubDate>
</item>
</channel>
</rss>