Symbolic Logic:Programming:ORM Attributes
The heart of an Object to Relational Mapping (ORM) is the implementation of Attributes.
Attributes map to columns in the database. Using Renaming Inheritance all of the functionality for implementing an attribute is implemented in one class, and shared out for use by the ORM Persistence classes.
- Access the value
- Get and Set methods to access the value.
- Validation
- SQL Select
- Construction of the select attribute list.
- Construction of where conditions to retrieve objects.
- Order by
- Interacting with the database.
- Retrieving column values from rows in a select.
- Acting as a parameter in calling stored procedures.
- Upgrading the Database
- Creating the column in the database.
- Creating the column in stored procedures
- Insert
- Update
- Delete
Once the Attribute class has done its job all the ORM has to do is combine the results.
Contents |
[edit] Attributes
To add an attribute to a Persistent object, inherit from the Attribute class like,
- inherit Attribute("name", String) as Name;
for example to create a persistent class with attributes name, age, sex,
- class Person
- {
- inherit Persist("person", Person)
- inherit Attribute("name", String) as Name;
- inherit Attribute("age", long) as Age;
- inherit Attribute("sex", EnumSex) as Sex;
- List(PersistBase) GetInheritingClasses()
- {
- return [new Person, new Lawyer];
- }
- }
A class inheriting from Person automatically inherits all the attributes of the person. For example,
- class Lawyer
- {
- inherit Persist("lawyer", Lawyer)
- inherit Person
- inherit Attribute("title", String) as Title;
- List(PersistBase) GetInheritingClasses()
- {
- return [new Lawyer];
- }
- }
When a database upgrade is requested the "lawyer" table will automatically be updated to include all the columns of the Person, as well as the attributes of Lawyer.
[edit] Accessing the attribute value
An attribute value changes with time. This is discussed in Applied Constraint Logic Programming.
The functions Get and Set may be defined as,
- class Attribute(attributeName, type)
- {
- inherit PersistBase;
- private:
- TimeList(type) m_ValueList
- protected:
- // Get the attribute value (at a particular time).
- type Get(role Time transactionalTime) rename as Get%
- {
- precondition !IsQueryTemplate();
- return m_ValueList.Find(transactionTime);
- }
- // Set the attribute value (at a particular time).
- type Set(type newValue, role Time transactionalTime) rename as Get%;
- {
- precondition !IsQueryTemplate();
- m_ValueList.Add(transactionTime, newValue);
- OnChange();
- }
- virtual Boolean Validate(role Time transactionalTime) rename as Validate {};
- virtual void OnChange(type oldValue, type, newValue, role Time transactionalTime) rename as OnChange% {};
- }
The above Get and Set functions are run only if Persist.CreateObject is used to create the object. This makes IsQueryTemplate() return true, which satisfies the precondition.
The transactionTime is the time at which the transaction occurred. Using roles it is not necessary to explicitly pass this time to each call.
The TimeList implementation may be expensive in performance. However as long as certain restrictions are allowed on the treatment of times it is not actually necessary to record the full list. Only the current and the original value are likely to be accessed. This functionality can all be hidden inside the TimeList service.
The IsDirty function plays says if the attribute value has changed since being retrieved from the database. The combine operator || is used to create an IsDirty function in the inheriting class that ors together the results of IsDirty on each attribute. This calculates if the row has been modified since being retrieved from the database.
[edit] Selection of Records
A select query on an object looks like,
- List(Person) oldies = Person.Select(x : x.GetAge() > 55);
The Select statement creates SQL which retrieves rows from the database table. To implement this the expression x.GetAge() > 55 returns a WhereCondition. The Select function uses the WhereCondition and other information from object to generate SQL like,
- SELECT id, timestamp, update_user,person, name, age, sex
- FROM age = 55
Another example is,
- Person p = new Person();
- List(Person) sociablePetOwners = Person.Select(x : x.IsSociable() and x.GetPetList().Count() > 0);
IsSociable is a function like,
- any IsSociable()
- {
- return GetFriendList().Count() > 20;
- }
A function like IsSociable has a dual interpretation,
- To return a true or false to say if a particular person is lonely.
- To construct SQL to retrieve a list of lonely people.
The keyword "any" supports this dual meaning by allowing us not to define the return type of the function. Instead the compiler must decide the return type from the context.
[edit] Implementation
Select is implemented in the PersistBase class as,
- class PersistBase
- {
- // Get a comma separate string of field names.
- abstract String GetAttributeNames();
- }
The attribute class has the following code for supporting selection,
- class Attribute(String attributeName, class type)
- {
- public:
- String GetAttributeName() rename as GetAttributeNames combine CommaCombine;
- {
- return attributeName;
- }
- protected:
- void Read(DBRow row)
- {
- Set(row.GetField(attributeName));
- }
- }
GetAttributeNames is used to retrieve a list of attribute names separated by commas. CommaCombine is defined in the PersistBase class.
Read is used to read a row of data from the query result set into a business object.
[edit] Construction of where conditions to retrieve objects
The Get function is overloaded in the attribute class,
- class Attribute(String attributeName, class type)
- {
- // Retrieve the table attribute name for use in constructring SQL queries.
- StringField(type) Get() rename as Get%
- {
- precondition IsQueryTemplate();
- return new StringField(type, this, attributeName);
- }
- }
The precondition IsQueryTemplate() enables the above implementation of the function only when the object is constructed using the Person.CreateTemplate() method. The Get method for retrieving attribute values is enabled when the object is created using Person.CreateObject().
The two Get functions have different signatures. Both signature are matched by the GetAge call, because the transactionTime is a role. Each call to GetAge has code to invoke both methods, but which method is invoked depends on the IsQueryTemplate() function.
[edit] Order by
TODO
Somebody must have thought of a really good way of building up order by conditions.
I want the duality of being able to create the nested inequalities, and also generating the order by condition.
[edit] Saving
- class Attribute(String attributeName, class type)
- {
- Boolean IsDirty(role Time transactionalTime) rename as IsDirty combine ||;
- {
- return m_ValueList.OriginalValue() != transactionalTime;
- }
- void Bind(DBBind bind)
- {
- bind.SetParameter(GetAttributeName(), Get());
- }
- }
[edit] Upgrading the Database
For the table the Attribute class needs to be able to add or modify a column.
- class Attribute(String attributeName, class type, bool nullable=true, long stringLength=2000)
- {
- protected:
- void UpgradeColumn(DBTable table, role Time currentTime) rename as UpgradeColumns
- {
- if (not table.ColumnExists(GetAttributeName())
- {
- table.AddColumn(GetAttributeName(), type.GetDatabaseTypeName(), nullable, length);
- }
- else
- {
- DBColumn column = table.GetColumn(GetAttributeName());
- if (column.GetType() != type.GetDatabaseTypeName())
- {
- column.SetType(type.GetDatabaseTypeName());
- }
- if (column.GetNullable() != nullable)
- {
- column.SetNullable(nullable);
- }
- if (column.GetLength() != length)
- {
- column.SetLength(length);
- }
- }
- }
- }