Skip to content
Kjell Nilsson edited this page Feb 28, 2023 · 52 revisions

Welcome to the LightObject wiki 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.

Contents

Functionality overview

Model driven Object structure

Business Objects

Using predicate to fetch objects

Faulting and lazy fetch

The REST like protocol

Common used API

Debugging

FAQ


More examples can be found at the Examples page..


Functionality overview


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 called LOArrayController. 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 a revert message. The saveChanges 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.


Model driven Object structure

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

Business Objects

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.


Using predicate to fetch objects

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"];

Faulting and lazy fetch

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
    }
];

The REST like protocol

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.


Common used API

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

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]

FAQ

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.

  1. 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.
  2. 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.