AdaCore Blog

Using reference types to handle persistent objects

by Maxim Reznik , Nicolas Setton

The Ada 2012 standard introduced user-defined references. The main idea behind this is simplifying the access to elements in a container. But you can use them to control the life-circle of your persistent objects. Let's see how it could work.

For example you have some object representing a user:

package Users is
   type User_Identifier is range 0 .. 2 ** 31 - 1;
end Users;

package Users.Objects is

   type User_Object is tagged limited private;

   function Get_Login
    (Self : User_Object'Class) return League.Strings.Universal_String;

private

   type User_Object is tagged limited record
      Identifier : User_Identifier;
      Login      : League.Strings.Universal_String;
   end record;

end Users.Objects;

You want to be able to store/retrieve a user object to/from a database. To do this you have the following package:

with SQL.Databases;

package Users.Objects.Stores is

   type User_Access is access all User_Object'Class;

   type User_Store is tagged limited private;

   procedure Initialize
    (Self     : in out User_Store'Class;
     Database : not null access SQL.Databases.SQL_Database'Class);

   not overriding function Get
    (Self       : in out User_Store;
     Identifier : User_Identifier) return User_Access;

   not overriding procedure Release
    (Self : in out User_Store; Object : User_Access);

private

   type User_Store is tagged limited record
      Database : access SQL.Databases.SQL_Database;
   end record;

end Users.Objects.Stores;

The subprogram Get loads an object into memory, and Release should be called when the application doesn't need it any more.

Such approach works, but it's cumbersome because you should manually release objects. Having circular dependencies between objects makes this even harder. Eventually this could result in that a lot of (not actually used) objects would be loaded into memory and stay there forever.

We could leverage user defined references here to simplify this task. We need two types

  • a reference to the object - User_Reference
  • and transient user-defined reference - Variable_Reference_Type
package Users.References is

   type User_Reference is tagged private;

   procedure Initialize
    (Self       : in out User_Reference'Class;
     Store      : not null access Users.Objects.Stores.User_Store'Class;
     Identifier : Users.User_Identifier);

   type Variable_Reference_Type
    (Object : not null access Users.Objects.User_Object'Class)
       is limited private
         with Implicit_Dereference => Object;

   function Object
    (Self : in out User_Reference'Class) return Variable_Reference_Type;

private

   type User_Reference is tagged record
     Store      : not null access Users.Objects.Stores.User_Store'Class;
     Identifier : Users.User_Identifier;
   end record;

   type Variable_Reference_Type
    (Object : not null access Users.Objects.User_Object'Class)
       is new Ada.Finalization.Limited_Controlled with
   record
     Store      : not null access Users.Objects.Stores.User_Store'Class;
   end record;

   procedure Finalize (Self : in out Variable_Reference_Type);

end Users.References;

As you can see, User_Reference doesn't contain a pointer to the user object. To reach the object it provides the method Object. Let's see an implementation of this package:

package body Users.References is

   procedure Finalize (Self : in out Variable_Reference_Type) is
   begin
      Self.Store.Release (Self.Object);
   end Finalize;

   procedure Initialize
    (Self       : in out User_Reference'Class;
     Store      : not null access Users.Objects.Stores.User_Store'Class;
     Identifier : Users.User_Identifier) is
   begin
      Self.Store      := Store;
      Self.Identifier := Identifier;
   end Initialize;

   function Object
    (Self : in out User_Reference'Class) return Variable_Reference_Type is
   begin
      return (Ada.Finalization.Limited_Controlled with
                Object => Self.Store.Get (Self.Identifier),
                Store  => Self.Store);
   end Object;

end Users.References;

A client using this package could look like:

with SQL.Databases;
with SQL.Options;

with Users.References;
with Users.Objects.Stores;

procedure Demo is
   Options  : SQL.Options.SQL_Options;
   Database : aliased SQL.Databases.SQL_Database
     := SQL.Databases.Create
         (League.Strings.To_Universal_String ("SQLITE3"), Options);

   Store    : aliased Users.Objects.Stores.User_Store;
   User     : Users.References.User_Reference;

begin
   Database.Open;
   Store.Initialize (Database'Unchecked_Access);

   User.Initialize (Store'Unchecked_Access, 0);

   Ada.Wide_Wide_Text_IO.Put_Line
    (User.Object.Get_Login.To_Wide_Wide_String);
end Demo;

When you call User.Object, a transient object of Variable_Reference_Type is returned. It points to User_Object through access discriminant. You don't need explicitly dereference it, thank to the Ada 2012 syntactic sugar. When the transient value is no longer need, the compiler will destroy it and will release the corresponding User_Object.

As result you do not need to keep every User_Object in memory. They will be loaded into memory on demand. Certainly, with little efforts you can improve Store implementation to cache objects and have acceptable performance.

Posted in #Ada 2012    #Persistent Objects    #Programming    

About Maxim Reznik

Maxim Reznik

Maxim Reznik is a Software Engineer and Consultant at AdaCore. At AdaCore, he works on the GNAT Studio, the Ada VS Code extension and the Language Server Protocol implementation.

About Nicolas Setton

Nicolas Setton

Nico joined AdaCore in 2001 as an engineer, and has worked on many interesting things since. Now lead of the DevOps-for-Customers team and head of the User Experience unit.