-
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 would 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
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 it 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.
Something interesting
Something interesting
Something interesting
Something interesting
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]
var fetchSpecification = [LOFetchSpecification fetchSpecificationForEntityNamed:@"Person" qualifier:nil];
[objectContext requestObjectsWithFetchSpecification:fetchSpecification withCompletionHandler:function(objects, statusCode) {
if (statusCode === 200) {
// Do something with the objects
}
}];
An operator is extra information that is sent to the backend. It is used for example to tell the backend to do a lazy fetch (just send the primary keys) or count the number of objects (just send a number with the number of rows).
The default behavior that is used with the objc-backend is to add the operator after the method in the url. Normal url for a fetch is http://localhost/backend/fetch/Person. If a lazy
operator is added the url will look like http://localhost/backend/fetch/lazy/Person
var fetchSpecification = [LOFetchSpecification fetchSpecificationForEntityNamed:@"Person" qualifier:nil];
[fetchSpecification setOperator:@"lazy"];
[objectContext requestObjectsWithFetchSpecification:fetchSpecification withCompletionHandler:function(objects, statusCode) {
if (statusCode === 200) {
// Do something with the objects
}
}];
The default behavior that is used with the objc-backend is to use the method fetch
to fetch objects. Normal url for a fetch is http://localhost/backend/fetch/Person. If a special method is implemented in the backend to send objects to the client the url will look like http://localhost/backend/MyMethod/Person. This is how to tell the LOF to use an alternative method to fetch the objects
var fetchSpecification = [LOFetchSpecification fetchSpecificationForEntityNamed:@"Person" qualifier:nil];
[fetchSpecification setMethod:@"MyMethod"];
[objectContext requestObjectsWithFetchSpecification:fetchSpecification withCompletionHandler:function(objects, statusCode) {
if (statusCode === 200) {
// Do something with the objects
}
}];
var fetchSpecification = [LOFetchSpecification fetchSpecificationForEntityNamed:@"Person" qualifier:nil];
[fetchSpecification setRequestPreProcessBlock:function(request) {
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[CPString JSONFromObject:{
sessionKey: theSessionKey,
itemKeys: itemKeys
}]];
}];
[objectContext requestObjectsWithFetchSpecification:fetchSpecification withCompletionHandler:function(objects, statusCode) {
if (statusCode === 200) {
// Do something with the objects
}
}];