DSAM

Romans Malinovskis edited this page Aug 27, 2016 · 17 revisions

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

Core Terms

  • 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.

Examples:

  • Mapping can convert $user model into a DSQL Query: select count(*) from user
  • Execution converts DSQL Query such as select count(*) from user into number 24.
  • Compose allows you to use DSQL Query select count(*) from user inside a Field "total" of $category Model.
  • 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.

Examples:

"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->addField('name');

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.

Persistence Terms:

  • 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)

Examples:

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: ['id'=>1, 'name'=>'John'].
  • Table Field: Always a physical field id or name. For JSON-based databases can be optional.

Record Term

  • 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.

DataSet Term:

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.

Example:

$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 $m.

$m->join('user_details.user_id')

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.

$m->addCondition('is_email_confirmed', true);

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 Term

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.

  • Example:

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 $m->id.
  • 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 unload()ed.

NOTE: do not confuse $m->delete() with $m->action('delete'). First Active Record from DataSet and the second deletes ALL Records from DataSet.

Examples:

Calling $m->load($id) will find a Record inside a DataSet and load Field Data into $m->data hash. The $id will be stored in $m->id.

You can interact with the Field Data through set()/get() model methods. When you call $m->save(), the modified data of Active Record will be saved inside DataSet.

Field-related Terms

  • 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.

Example:

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 $m->dirty property.

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.

Reference Terms

  • 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.

Reference_HasOne Reference_HasMany Generic
Active Record Traversal loaded/unloaded unloaded as decided inside call-back
DataSet Traversal unloaded unloaded as decided inside call-back
Linking unloaded unloaded unsupported

Example:

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.

$user->load(1)->ref('category_id')->export()

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:

$user->withID(1)->ref('category_id')->export()

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');

Now, 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 Terms

  • 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 $this inside 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.

Importing Fields

  • 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