-
Notifications
You must be signed in to change notification settings - Fork 5
Home
This wiki will try to cover the ins and outs of the LightObject framework. The LightObject framework is a Objective-J framework that can be included in a Cappuccino application to request for and receive JSON structured data that will be transformed into Objective-J objects in the application. LightObject is a lightweight framework based on the concept found in Apples Enterprise Objects Framework (EOF) used in WebObjects. There are also similarities with Apples CoreData framework used in MacOSX desktop applications for easy storing and retrieving of information as objects. Although the named frameworks are so called Object Relational Mapping (ORM) frameworks the LightObject does not map against a relational database, but to a specific JSON data structure. It also uses a REST like protocol when talking to the data source. The datasource can reside on any place defined with an URL. Similar to the CoreData framework it is possible to use local datasources other than DMBS, like local files or databases, if a custom LOObjectStore is implemented.
To fetch data one will need some kind of server backend that handles the requests for data used in the client application. There is a simple generic backend application running under node.js that can be downloaded from GitHub, it's called objj-backend. It uses a PostgreSQL database engine to store the data.
Using predicate to fetch objects
More examples can be found at the Examples page..
The LightObject framework has a wide spectra of functionality for constructing and accessing object structures.
-
A model driven metadata definition of the content and the structure of the data using an Xcode model compliant definition.
It's possible to create the metadata model in Xcode model builder or by hand and let this model reside in the application or on the server. When the application initialises the LightObject framework asks the backend for the model.
Models consists of entities, attributes and relationships to other entities. Each of there have properties that tells the LightObject framework how it should handle them in different situations.
See more in the chapter named Model driven Object structure.
-
A fetch is defined by specifying the entity and in cases a predicate and/or an operator.
A fetch is defined using a
LOFetchSpecification
that can have predicates and operators. This makes it easy to fetch the a count or a sorted list of objects if the server backend can handle it. A simple fetch specification can be done like this:Example:
var fetchSpecification = [LOFetchSpecification fetchSpecificationForEntityNamed:@"Person"];
See more in the chapter named Common used API and in the chapter named Using predicate to fetch objects.
-
Filtering objects is done using a
CPPredicate
.So accessing an object using a primary key or filtering objects of a special entity is done by constructing a predicate similar to the WHERE clause in an SQL statement. It can be an easy predicate like "socialSecurityNumber = '2420129'" or a more complicated one like "firstname = 'Marion' and 'surname like 'M*''".
Example:
var predicate = [CPPredicate predicateWithFormat: @"firstname like 'J*' and 'surname = 'Bach'"];
-
Fetching objects or object graphs can be handled by lazy fetching
One really powerful feature in the LOF is the ability to handle ’lazy’ fetch where not the actual objects are fetched rather only the primaryKey is transfered from the backend and made into a fault object in the client. As soon as an attribute is accessed the fault gets triggered and the whole object is fetched. This saves a lot of transferring when using lists of a big number of objects. Since the transfer is done asynchronously bindings to the views are to be preferred since then the UI gets updated automatically when the trigger finishes.
-
Objects are locally stored in an
LOObjectContext
to keep track of changes done.When objects are fetched they are registered in the
LOObjectContext
. This context keeps track of changes done to the objects and can also tell if the object is stored or newly created. Other methods in this class will create new objects of a given type, insert objects and delete objects in the context. It also handles the relationship between the objects. -
Requesting objects asynchronously from backend datasource are also done using an
LOObjectContext
.Since the backend data source probably resides at a server elsewhere than the application LightObject is using an asynchronous protocol to request data. Therefore the
LOObjectContext
implements request methods using callbacks. This is nicely handled using a delegate or preferably using a completion handler.Example:
var objectContext = [[LOObjectContext alloc] init]; var objectStore = [[LOBackendObjectStore alloc] init]; [objectContext setObjectStore:objectStore]; var predicate = [CPPredicate predicateWithFormat: @"firstname like 'J*' and 'surname = 'Bach'"]; var fetchSpecification = [LOFetchSpecification fetchSpecificationForEntityNamed:@"Person"]; [fetchSpecification setQualifier:predicate]; [objectContext requestObjectsWithFetchSpecification:fetchSpecification withCompletionHandler:function(persons, statusCode) { if (statusCode === 200 && [persons count] > 0) myFetchedPersons = persons; }];
-
Objects can be kept in array controllers and object controllers for easy access.
Since bindings is one of the key features in a Cappuccino application LightObject implements a subclass of
CPArrayController
calledLOArrayController
. Using this controller, in connection with the CPObjectController and bindings to user interface views, it is easy to setup a user interface either using the Xcode InterfaceBuilder or using code. This is the key feature for creating a whole CRUD application in InterfaceBuilder using no code at all. -
Object changes can be automatically stored using auto commit or be handled as transactions.
By default the object context is set to auto commit where all changes will be save when the change occur. But it can also be set to not autocommit, in that case the object context stores all changes until it gets either a
saveChanges
message or arevert
message. ThesaveChanges
message creates a JSON structure with the changes, whether inserts, changes or deletes, and sends them via the object store to the backend. It looks fore that the changes is done in the right order. -
When relationship between entites in the model are present, fetching objects creates faults to the related objects.
In order to handle fetching objects with relationship to other objects, LightObject framework creates faults to the related object. Faults are not triggered until they are accessed. This is crucial to avoid all data in the database. Since all requests are handled asynchronously it is important to make sure code that is depending on the actual values is runned after the fault is completely fetched. In case binding is used the binding will update the UI when the fault is completely fetched.
The ability to create a data model in Xcode is timesaving and it can also give the modeler a birds view of the the applications data. What is created is actually a directory with the appendix .xcdatamodeld that contains the XML file that defines the model. Of cause this XML file can be constructed without Xcode as well. The XML structure is explained
Here is a much simplified standard .xcdatamodeld content file example:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="10171" systemVersion="15E65" minimumToolsVersion="Xcode 7.0">
<entity name="Organization" syncable="YES">
<attribute name="city" optional="YES" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="streetaddress" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="employees" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Person" inverseName="organization" inverseEntity="Person" syncable="YES"/>
</entity>
<entity name="Person" syncable="YES">
<attribute name="firstname" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="surname" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="organization" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Organization" inverseName="employees" inverseEntity="Organization" syncable="YES"/>
</entity>
</model>
LightObject reads this file so it can understand the data receive from a backend, it uses it to create and populate the objects in the application. Only a subset of the info CoreData uses is valid in LightObject.
In the model it is possible to add properties to entities, attributes and relationships. Some of these properties that Xcode visualize are not used and some is not present in Xcode to set other than if you name them yourself in the user info part of the definition. A list of the properties used are explained here:
Name | Property | Explanation | Optional | |
---|---|---|---|---|
Entity | Name | name | Name of the entity | No |
Abstract | isAbstract | Flag if abstract | Yes | |
Parent | parentEntity | Name of parent entity | Not used | |
•Class | Name | representedClassName | Name of the entity class | Yes |
Modul | Not used | |||
Codegen | Not used | |||
•UserInfo | entityName | Local entity name | ||
parentEntity | Super entity name | |||
Attribute | Name | name | Name of the attribute | No |
Transient | transient | Flag if not saved | Yes | |
Optional | optional | Flag if NULL is allowed in DB | Yes | |
Type | attributeType | Type of data | No | |
Validation | Not used | |||
Max/Min length | Not used | |||
Default value | defaultValueString | Default value on attribute | Not used* | |
•UserInfo | foreignKey | Foreign key to one | ||
valueTransformerName | Name on transformer | Not used | ||
Relationship | Name | name | Name of the relationship | No |
Transient | transient | Flag if not saved | Yes | |
Optional | optional | Flag if NULL is allowed in DB | Yes | |
Destination | destinationEntity | Destination Entity name | No | |
•UserInfo | primaryKey | Primary key to many | ||
foreignKey | Foreign key to one |
The design of the LOF is that a Business Object can be any class. It does not need to inherit from anything like Core Data's NSManagedObject. It only has to be Key Value Coding compliant. CPObject is Key Value Coding compliant so the easiest way is to just inherit from CPObject. This allows any class like CPMutableDictionary to be a Business Object.
After setting up the LOF with an object store and an object context, the next thing that probably to be done is request data. LOF handles requests using fetching. The whole request/connection handling is done by the LOF so what the LOF user has to do is setting up a fetch specification and then tell the object context to request the data using the fetch specification.
The fetch specification has a number of ways to configure its task: First it needs to be handled the entity name of the objects to be fetched using one of the following methods.
(LOFetchSpecification) fetchSpecificationForEntityNamed:(CPString) anEntityName
or
+ (LOFetchSpecification) fetchSpecificationForEntityNamed:(CPString) anEntityName qualifier:(CPPredicate) aQualifier
Second it has to be feed a qualifier, a predicate, if it should not fetch the all the objects of that entity. This predicate will filter the fetch result with the setup of the predicate. Like:
predicate = [CPPredicate keyPath:@”primaryKey” equalsConstantValue:aPrimaryKey];
or like:
predicate1 = [CPPredicate keyPath:@"personForeignKey" equalsConstantValue:[thePerson uuid]];
predicate2 = [CPPredicate keyPath:@"lastName" notEqualsConstantValue:[somePerson name]];
predicate = [CPCompoundPredicate andPredicateWithSubpredicates:[predicate1, predicate2]];
or other predicates more complex.
Third it could be supplied an operator that tells the backend how to respond to the request. The most common operator I ”count” that just returns the number of hits the fetch will return without operator.
[fetchSpecification setOperator: @"count"];
One really powerful feature is that it can create faults for related objects. So if a order object has a relation to a list of order items, when the order is fetched the property orderItems will automatically be populated with an array fault. The fault will trigger a new fetch on all the objects order items, when accessed the first time. Same if the relation object is a ”to one” relationship, but then an object fault is created.
To trigger a fault in advance there is a method in the object context that does this and takes a completionHandler as argument so it will be easy to read what happens when the trigger has executed and the real object has been read in. To check out if an object is a fault one can use the protocol to find out: isFault = [obj conformsToProtocol:@protocol(LOFault)];
Here is an example how to trigger the fault:
coapplicant = [aMember valueForKey:@"coapplicant"];
[objectContext triggerFault:coapplicant withCompletionHandler:function()
{
// Continue processing
}
];
When a business object held by the object context is created or if it is deleted or altered, the LOF keeps track of all these changes. Then when it gets the saveChanges message it posts these changes to the backend for storage. In a complex save all three changes have to be handled: inserts, updates and deletions. So the object store, when gathering the object contexts changes, it loops through these changes and creates 3 arrays one with inserts, one with updates and one with deletions. When creating these arrays the object store removes inserts and updates for objects that has been deleted. The method that actually saves the changes to the backend is the object store method saveChangesWithObjectContext:withCompletionHandler:, and it creates a JSON structure that gets posted to the backend.
Example on an JSON array with changes:
[
{"inserts":[{"name":"TestKonstant","explanation":"Konstant för testning","value":"2","group":"1","_tmpid":"0","_type":"bofConstant"}],"sessionKey":"CguJvwR7RBepzjXSz2o0Ylf1K9V"}
{”updates":[{"value":"5","_type":"bofConstant","primaryKey":"5BB758F2FC2D0003AA9B07EC"}],"sessionKey":"CguJvwR7RBepzjXSz2o0Ylf1K9V"}
{”deletes":[{"_type":"bofConstant","primaryKey":"5BB758F2FC2D0003AA9B07EC"}],"sessionKey":"CguJvwR7RBepzjXSz2o0Ylf1K9V"}
]
When the backend receives this array in JSON form it sorts it out and stores these changes into the database.
Creating ObjectStore
objectStore = [[BOPObjectStore alloc] initWithModel:LOF_MODEL];
Creating ObjectContext
objectContext = [[LOObjectContext alloc] initWithDelegate:self];
[objectContext setObjectStore:objectStore];
Configuration of object context
[objectContext setAutoCommit:NO];
[objectContext setSharedObjectContext:SharedObjectContext];
FetchSpecification setup
predicate = [BTPredicate keyPath:@"primaryKey" equalsConstantValue:anOrganizationId];
fetchSpecification = [LOFetchSpecification fetchSpecificationForEntityNamed:@”Person”];
[fetchSpecification setQualifier:predicate];
fetchSpecification = [LOFetchSpecification fetchSpecificationForEntityNamed:@"Person" qualifier:predicate];
Requesthandling
[objectContext requestObjectsWithFetchSpecification:fetchSpecification withCompletionHandler:function(objects, statusCode) {
if (statusCode === 200) { . . . }
}];
Saving
[objectContext saveChanges];
and its delegate. …
- (BOOL)objectContext:(LOObjectContext)context shouldSaveChanges:(CPDictionary)changes withObject:(id)anObject inserted:(BOOL)isInserted {
var shouldSave = YES;
// We only check for save if there is changes to the object or if it is newly created
if ((changes && [changes count] > 0) || isInserted) {
shouldSave = [self validateObjectForSave:anObject]; // For example …
}
return shouldSave;
}
Debugging communication with the backend and bindings with UI controls is done by setting method setDebugMode:
on the LOObject.
Here are the available modes:
LOObjectContextDebugModeFetch
LOObjectContextDebugModeSaveChanges
LOObjectContextDebugModeReceiveData
LOObjectContextDebugModeObserveValue
LOObjectContextDebugModeAllInfo
Example:
[objectContext setDebugMode:LOObjectContextDebugModeSaveChanges | LOObjectContextDebugModeObserveValue]
How can I store the data somewhere else?
The LOF has the concept of Entity with Attributes. It also has the option to use Relationships between these Entities. Every entity has also the concept of rows that each is identifiable by an id. In the client these are represented as regular Objective-J objects with attributes. A query is usually done with CPPredicates. The LOF has an Object Store layer that handles how the data is stored. The current implementation talks with a backend using a proprietary JSON format. The objj-backend is an open source backend implementation in Objective-J that talks this proprietary JSON format. Currently the backend stores the data in a Postgres database but it is easy to implement other sql or even no sql databases as an adaptor in the backend. There are two ways to store the data somewhere else.
- Write your own Object Store for the LOF framework. This runs on the client and can talk to your own backend. This has to deal with converting the data to objects when fetching and handle the changed data that needs to be sent to the backend when saving. It is also possible to write an Object Store that will save the data in the browser's local storage instead of sending it to a remote backend.
- Write your own Database Adaptor in the objj-backend. This is just an Objective-J class that has to implement a protocol. This adaptor handles the communication with the database. The queries are done with a CPPredicate so the adaptor has to be able to convert a CPPredicate to whatever the database uses. You can use any database but it has to be able to map the concepts of Entity with Attributes and rows that is identifiable with an id as described above. It can be a sql database or a no sql database. You can even store the data in regular files like a xml files or plain flat files.
Sure it is easier to use a rational database to store the data as the concepts match very well but as described above it is possible to use almost anything. The LOF framework has a lot of features that will make your life easier but you have to remember that it also has its limitations. Don't use it if your data don't match the concepts above.