Clone this wiki locally
This is the specification for DataSet and Action Mapping pattern. It is considered a BluePrint for implementing Agile Data.
- Status: Draft
- Valid for Agile Data v 1.1
- Mapping - converting a vendor-independent object into a vendor-specific object.
- Execution - performing vendor query as described by a vendor-specific object.
- Composing - using vendor-specific object do describe a vendor-independent object.
- Feature - a vendor-specific object that is implementing a vendor-independent interface.
- Mapping can convert
$usermodel into a DSQL Query:
select count(*) from user
- Execution converts DSQL Query such as
select count(*) from userinto number
- Compose allows you to use DSQL Query
select count(*) from userinside a Field "total" of
- Mapping of Field "total" will convert it back into DSQL Query.
"Expression" is a Feature. SQL_Expression is a specific field extension for SQL database models that maps into DSQL Query. The vendor-independent Model sees SQL_Expression as a regular field. When using NoSQL database it's possible that some other class will be used to describe expressions.
When defining a relationship between 2 models that both use same Database they can take advantage of SQL-capabilities and embed DSQL Query generated by Expression into their own. However, if models use different Database then they will Execute Expression instead and will rely on the results. To illustrate this:
$audit = $user->ref('Order')->ref('Audit')->export();
If in the above chain all the models are associated with same SQL database, then only a single query will be used:
SELECT [fields] from audit where order_id in (select id from order where user_id = :A)
Now suppose that Audit model uses MongoDB for scalability. When traversing from Order into Audit, reference will notice that sub-select Feature cannot be used inside MongoDB, so it will execute it converting into list of IDs, which will then be used as a condition for MongoDB Query.
Domain Model Terms
- Model - an object representing a business entity.
- Field - Feature describing a property of a business entity.
- Join - Feature describing a pattern between database Tables.
- Reference - Feature describing a pattern between Models.
- Persistence - Feature containing core Mapping logic.
- Association - Process in which Model is linked with Persistence.
- Initialize - execution of
init()prepares Model to be used with selected Persistence.
"Model" is an object. We can create it
new Client() and pass around. It can be safely cloned and later associated with Persistence. Init is not executed until the Association.
When Model is Initialized, Persistence can choose a Class to implement Features such as Fields, Joins, Expressions and Relations. This class must implement the basic interface so that Model could interact with the class. For instance:
This will create an object of Field_SQL or just Field - depending on Persistence. However, regardless of the choice, this object must work in a certain way.
Join implements a strategy allows Model to load / save Record data into multiple Tables Rows and maintain relations. When you use this with Persistence_SQL, Join_SQL will affect Mapping, by including "JOIN" keyword into DSQL Query. On the other hand, when used with Persistence_Array, Join_Array will affect the execution of "load" action and copy some data from related Table into Active Record.
- Database - a 3rd party storage mechanism for long-term data storage (can be DB, API, Array or Folder)
- Table - identifier describing name of table/collection/file inside a database
- ID - identifier that points towards a structured data inside a specific table
- Table Row - structure data that can be addressed through the combination of Database+Table+ID.
- Table Field - column or field inside Table Row (works with schema-less DB)
You need to start by initializing Persistence then associating Models with it. Persistence_SQL connects to an RDBMS databases. Persistence_Array considers an array that you specify to be a database. There may be many other types of Persistences and they can implement anything into a Database.
- Table: "user", "order" or "user_data"
- ID: can be numeric, hash or even something else. ID may map into Table Field, but that's not necessary.
- Table Row: basically a Hash:
- Table Field: Always a physical field
name. For JSON-based databases can be optional.
- Record: structured data addressed through the combination of Model+ID
- Field Data: One component of a record addressable by Field name name.
It is important to note that Record in Agile Data is not same as Table Row. It may look similar (Record is also a Hash: `['name'=>'John']), however Records lives in Domain model and is persisistence-independent:
- Record can contain rows from multiple joined tables: "user join user_data on user_data.user_id = user.id"
- Field Data can come form Table Row, SQL expressions or calculated manually during loading action.
- Type of Field Data will use PHP standard types such as DateTime as opposed to Table Field, which is vendor-dependant.
- In most cases, however, Record field will have a corresponding Table field, but even in this case the name of Table Field can be different to Record Field.
Finally, we have enough basic terms to fully define DataSet and Actions.
- Condition - a special clause defined for Field or through Expression Feature that can determine if Table Row is eligible.
- DataSet - collection of Records for which respective Table Rows will satisfy all Conditions defined for a Model
- Order - defines Record Order within DataSet by using Field or through Expression Feature.
- Limit - defines offset/max Records, but will only affect "select" (iterating) action.
$m = new User($db);
Assuming that $m didn't apply any Condition during Initialization, and that Model
$m is using Table 'user', then all Rows inside
user will map into Records for the DataSet of
Using a straight Join Feature will affect the DataSet. A Row that does not have a corresponding Row inside user_details Table will no longer be eligible.
Adding a Condition is another way how to narrow down a DataSet.
is_email_confirmed refers to a Field, which will be Mapped into a DSQL expression used to test Row eligibility.
Action describes an operation that may affect one or multiple records inside DataSet. This may be a read operation or delete operation.
It's important to note that Action has no access to Records that are outside of DataSet.
action('delete') Maps into a DSQL Query for deleting all Records from DataSet. Similarly
action('count') maps into an aggregation query that returns number of records in DataSet when Executed.
Active Record Term
- Active Record - the in-memory copy of a single Record with ID of
- Active Record Actions - a special set of actions for implementing Load, Iterate, Update, Replace and Delete operations.
- Loaded Model - A state of a Model where Active record have been loaded from DataSet.
- Unloaded Model - A default state of a Model, where Active Record has not been loaded yet or have been explicitly
NOTE: do not confuse
$m->action('delete'). First Active Record from DataSet and the second deletes ALL Records from DataSet.
$m->load($id) will find a Record inside a DataSet and load Field Data into
$m->data hash. The
$id will be stored in
You can interact with the Field Data through
get() model methods. When you call
$m->save(), the modified data of Active Record will be saved inside DataSet.
- Default Value: field property that is set to non-null value. This value will be used by save() and get() when Active Record wasn't originally loaded and when Field Data wasn't explicitly specified by set(). It will also be used if schema-less persistence didn't have a value for Field when loading.
- Dirty Field: indicates that Field of Active Record has different value to original "loaded" value or "default" value if Active Record wasn't loaded.
- Active Record is considered Dirty if at least one field is Dirty.
- If Save Action is executed on an Active Record that is NOT dirty (even after calling before-save hooks), it will be silently skipped.
- Field flags: describes various properties of a field, such as "read_only", "never_persist" or "never_save". If all Dirty Fields of Active Record have properties of either never_persist or never_save, then Active Record is not considered Dirty.
- Dirty fields are no longer Dirty after successful save action, even if Field value wasn't actually stored into Table Field because of Field Flag.
$id_field- contains the name of Table (TODO: unsure, may be Model Field) Field which is considered to be ID. Defaults to "id".
$title_field- contains a name of Model Field, that better represents "title" of a Record. Defaults to "name" but does not have to be necesserily defined, as functionlity relying on "Title Field" will have a fail-safe mechanisms around it.
When Active Record is loaded, only a single copy of the record is stored in-memory. When you access Field Data through set/get, the original Field Data will be preserved inside the
Active Record Save action will map into DSQL Query that will have SET clause for the corresponding Dirty Fields only. Join feature will divert Save Action towards an appropriate Table/Row/Field.
- Referenced Model: describes a Model (Initialized or Not) that has some business-logic association with a current model.
- Reference Definition: act of describing how exactly Referenced Model relates to current model.
For SQL There are 3 types of Reference Definition and 3 types of Reference Traversal. New definition types can be added.
- Reference_hasMany - indicates that single Record of a model relates to DataSet consisting of 0..many records of a Referenced Model that is Initialized with the same Persistence.
- Reference_hasOne - indicates that single Record of a model may have 0 or 1 Records inside the DataSet of Referenced Model that is Initialized with the same Persistence.
- Generic Reference - indicates that single Record relates to DataSet of a specific Model, potentially associated with a different persistence.
- Flag "mandatory" can be used on a field used by Reference_hasOne, to ensure that exactly 1 record is referenced.
There is no need for many-to-many or any other relation types because any of the above references can be used with different types of traversal:
- Active Record Traversal - requesting Reference to return a Model with DataSet (and potentially loaded Active Record) that corresponds to Active Record of source Model.
- DataSet Traversal - requesting Reference to return a Model with DataSet that corresponds to the DataSet of Source Model.
- Linking - requesting Reference to return a Model with a DataSet that will map into Query that can be used to Compose an Expression usable in Source Model Mapping (SubQuery).
The following matrix will describe if you will receive a Loaded or Unloaded Model after traversal.
|Active Record Traversal||loaded/unloaded||unloaded||as decided inside call-back|
|DataSet Traversal||unloaded||unloaded||as decided inside call-back|
Category hasMany User. User hasOne category.
$category->load(1)->ref('User') // All users which belong to category 1 // WHERE user.category_id = 1 // If there is no category 1, will throw exception. $category->unload()->ref('User') // All users that have a category that fall into DataSet of $category. // WHERE user.category in (select id from category <..where..>) // If dataSet of $category is empty, will result in empty DataSet for User. $category->withID(1)->ref('User') // Same as with load(1) but without extra query. // WHERE user.category in (select id from category <..where..> AND id=1) // If there is no category 1, will result in empty DataSet for User. $user->load(1)->ref('category_id') // Will load category for a corresponding user // WHERE ID=category_id // If there is no user 1, load() will throw exception // If category_id is null, returns Unloaded Model // If category_id is null, and is mandatory and if active record was loaded will Throw Exception // If you execute action save() to add category, will automatically populate // Field Data to ID of newly added category inside Active Record of source model // If you execute action load() to change category, will automatically populate // Field Data to ID of newly loaded category inside Active Record of source model // If Active Record is changed, you have to explicitly save it. $user->unload()->ref('category_id') // DataSet of Category consisting of all Records that is used at least by // one Record inside DataSet of source model. // WHERE ID in (select user.category_id <..where..>) $user->withID(1)->ref('category_id'); // DataSet of Category consisting of a 0 or 1 Record which ID is equals to the // category Mapped from the DataSet of User Model which consits of 0 or 1 record // where Record ID equals to 1. // WHERE ID in (select user.category_id <..where..> AND user.id = 1) // This is similar to the load(1) approach above, but with few differences: // - You'll have to call tryLoadAny() to load that potential record // - Adding new record will not affect anything
I must also talk about the DataSet of hasOne traversal of Active Record.
Even though this type of traversal will load Active Record inside Category model, the DataSet will not be Conditioned. Calling export() like demonstrated above will output full DataSet for Category Records that are eligible to be stored inside category_id.
This is done so that you could present all the possible category options inside a "DropDown" but also to validate that user-selected option is indeed a good category. This does not affect any conditions added when specifying a Referenced Model.
Next, if you execute this code:
You will receive the maximum of one Record because DataSet will be conditioned.
Non-ID field referencing
When defining references, we normally assume that hasOne will mean
source.target_id = target.id and for hasMany
source.id = target.source_id. It is, however, possible to change both "our_field" and "their_field" for each Reference Definition. This can result in scenarios like this:
$user->hasOne('currency_code', [new Currency(), 'their_field'=>'code']); $user->load(1)->ref('country_code');
code is not necessarily a unique field, so during traversal, an attempt to load Active Record for Currency may have several matching records. Only the first record will be loaded, so be sure to properly define Record Order.
- Hook is an action that can be executed at certain times while working with a Model.
- Hook is defined by a 'model object', 'spot', 'priority' and 'callable'
- Cloning a model will NOT clone hooks, but will keep their references. Never use
$thisinside a hook if you plan to Clone model.
- Domain Model hooks operate with Model Fields regardless of Associated Persistence.
- Persistence Hooks operate with Actions/Queries after they are Mapped into a vendor-specific object.
- Imported Field: Field that is added to a model by using a Field inside a related model
- Dereferencing: Removal of "_id" from the end of the field.
- Title Field Import: Will import a "title" field from related Model into Dereferenced $our_field and will also implement "lookup-on-save" strategy.
- Aggregate Field: Imported field that is wrapped inside aggregate function (e.g. sum())
- Lookup-on-save: Instead of specifying ID of related records allows you to specify it's Title field.
Lookup-on-save needs some clarification:
$user->hasOne('currency_id', new Currency())->addTitle();
Calling addTitle here will import a Title Field and call it by dereferencing "currency_id" into "currency". This title will additionally register a hook. If you omit 'currency_id' but specify 'currency' value instead, then Reference will map sub-query for "id" from Currency with a specific Name condition into the value of Field "currency_id" before saving.
Field importing is Feature implemented by Reference_SQL_One and Aggregate Field is implemented by Reference_SQL_Many.
TO BE CONTINUED