Rude class definition
Attributes do appear in Delphi 2010, and it is worth saying that FPC has an alternative syntax. Older versions of Delphi (still very deployed) do not have attributes available in the language, so it was not possible to be compatible with Delphi 6 up to latest versions (as we wished for our units).
It is perfectly right to speak about 'mis-use of index
' - but
this was the easiest and only way we found out to have such information, just
using RTTI. Since this parameter was ignored and not used for most classes, it
was re-used (also for dynamic array properties, to have faster lookup).
There is another "mis-use" for the "stored false
" property, which
is used to identify unique mandatory columns.
Using attributes is one of the most common way of describing tables in most
ORMs.
On the other hand, some coders have a concern about such class
definitions.
They are mixing DB and logic: you are somewhat polluting the business-level
class definition with DB-related stuff.
That is why other kind of ORMs provide a way of mapping classes to tables
using external files (some ORMs provide both ways of definition).
And why those days, even code gurus identified the attributes overuse as a
potential weakness of code maintainability.
Attributes do have a huge downsize, when you are dealing with a Client-Server
ORM, like ours: on the Client side, those attributes are pointless (client does
not need to know anything about the database), and you need to link to all the
DB plumbing code to your application. For mORMot, it was some kind of
strong argument.
For the very same reasons, the column definitions (uniqueness, indexes,
required) are managed in mORMot at two levels:
- At ORM level for DB related stuff (like indexes, which is a
DB feature, not a business feature);
- At Model level for Business related stuff (like uniqueness,
validators and filters).
When you take a look at the supplied validators and filters - see this article - you'll find out that this is much powerful than the attributes available in "classic" ORMs: how could you validate an entry to be an email, or to match a pattern, or to ensure that it will be stored in uppercase within the DB?
Other question worth asking is about the security.
If you access the data remotely, a global access to the DB is certainly not
enough. Our framework handle per-table CRUD level access for its ORM, above the
DB layer (and has also complete security attributes for services). It works
however the underneath DB grants are defined (even an DB with no user rights -
like in-memory or SQLite3 is able to do it).
The mORMot point of view (which is not the only one), is to let the DB persist the data, as safe and efficient as possible, but rely on higher levels layers to implement the business logic. It will make it pretty database-agnostic (you can even not use a SQL database at all), and will make the framework code easier to debug and maintain, since we don't have to deal with all the DB engine particularities. In short, this is the REST point of view, and main cause of success: CRUD is enough.
About the fact that you need to inherit from TSQLRecord
, and
can't persist anything, our purpose was in fact very similar to the "Layer
Supertype" pattern of Domain-Driven-Design, as explained by Martin
Fowler:
It's not uncommon for all the objects in a layer to have methods you
don't want to have duplicated throughout the system. You can move all of this
behavior into a common Layer Supertype.
Several ORMs at once
To be clear, mORMot offers three kind of table definitions:
- Via TSQLRecord
/ TSQLRecordVirtual
"native ORM"
classes: data storage is using either fast in-memory lists via
TSQLRestServerStaticInMemory
, either SQLite3 tables (in
memory, on file, or virtual). In this case, we do not use index
for strings (column length is not used by any of those engines).
- Via TSQLRecordExternal
"external ORM-managed" classes: DB tables
are created by the ORM, via SQL - see this article.
These classes will allow creation of tables in any supported external database
engine (SQlite3, Oracle, MS SQL, Jet, whatever OleDB
provider). In this case, we use index
for text column length. This
is the only needed parameter to be defined for such a basic implementation, in
regard to TSQLRecord
kind of classes.
- Via TSQLRecordMappedAutoID
/
TSQLRecordMappedForcedID
"external mapped" classes: DB tables are
not created by the ORM, but already existing in the DB, with sometimes a very
complex layout. This feature is not yet implemented, but on the road-map. For
this kind of classes we won't probably use attributes, nor even external files,
but we will rely on definition from code, either with a fluent definition,
either with dedicated classes (or interface).
The concern of not being able to persist any class (it needs to inherit from
TSQLRecord
) does perfectly make sense.
On the other hand, from the implementation point of view, it is very powerful, since you have a lot of methods included within this class definition. It does also make sense to have a common ancestor able to identify all three kind of mORMot's table definitions: the same abstract ancestor is used, and clients won't even need to know that they are implemented in-memory, using a SQLite3 engine, or even a MS SQL / Oracle database. Another benefit of using a parent class is to enforce code safety using Delphi's strong typing: you won't be able to pass a non-persistent type to methods which expect one.
From the Domain-Driven / SOA point of view, it is now an established rule to make a distinction between DTO (Data Transfer Objects) and Domain Values (Entity objects or Aggregates). In most implementations, persistence objects (aka ORM objects) should be either the aggregate roots themselves (you do not store Entity objects and even worse DTOs), either dedicated classes. Do not mix layers, unless you like your software to be a maintenance nightmare!
Some Event-Sourcing
architectures even implement several DB back-end at once:
- It will store the status on one DB (e.g. high-performance in-memory) for most
common requests to be immediate;
- And store the modification events in another ACID DB (e.g. SQLite3,
MS SQL or Oracle);
- And even sometimes fill some dedicated consolidation DBs for further
analysis.
AFAIK it could be possible to directly access ORM objects remotely (e.g. the consolidation DB), mostly in a read-only way, for dedicated reporting, e.g. from consolidated data - this is one potential CQRS implementation pattern with mORMot. Thanks to the framework security, remote access will be safe: your clients won't be able to change the consolidation DB content!
As can be easily guessed, such design models are far away from a basic ORM built only for class persistence.
The good ORM is the one you need
Therefore, we may sum up some potential use of ORM, depending of your
intent:
- If your understanding of ORM is just to persist some existing objects,
mORMot won't help you directly (but we identified that some users are
using the built-in JSON serialization feature of the framework to create their
own dedicated Client-Server ORM-like platform);
- If you want to persist some data objects (not tied to complex business
logic), the framework will be a light and fast candidate, via SQLite3,
Oracle, MS SQL or even with no SQL engine, using
TSQLRestServerStaticInMemory
class which is able to persist its
content with small files - see this
article;
- If you need (perhaps not now, but probably in the future) to create some kind
of scalable domain-driven architecture, you'll have all needed features at hand
with mORMot;
- If your expectation is to map an existing complex DB, mORMot will
handle it soon (it is planned and prepared within the framework
architecture).
Therefore, mORmot is not just an ORM, nor just a "classic" ORM.
Feedback is welcome on our forum, as usual.