Skip to main content
Version: Next

Data Model Objects

The ORDA Concept

ORDA stands for Object Relational Data Access. It is an enhanced technology allowing to access both the model and the data of a database through objects.

Relations are transparently included in the concept, in combination with lazy loading, to remove all the typical hassles of data selection or transfer from the developer. With ORDA, data is accessed through an abstraction layer, the datastore. A datastore is an object that provides an interface to the database model and data through objects and classes. For example, a table is mapped to a dataclass object, a field is an attribute of a dataclass, and records are accessed through entities and entity selections.

A query returns a list of entities called an entity selection, which fulfills the role of a SQL query’s row set. The difference is that each entity "knows" where it belongs in the data model and "understands" its relationship to all other entities. This means that a developer does not need to explain in a query how to relate the various pieces of information, nor in an update how to write modified values back to the relational structure.

Basically, ORDA handles objects. In ORDA, all main concepts, including the datastore itself, are available through objects. The datastore is automatically mapped upon the underlying database structure.

ORDA objects can be handled like standard objects, but they automatically benefit from specific properties and methods. ORDA objects are created and instanciated when necessary (you do not need to create them). However, ORDA data model objects are associated with classes where you can add custom functions and define calculated attributes.

Database as Objects

The ORDA technology is based upon an automatic mapping of an underlying relational database structure to a data model (this concept can be viewed as an included and enhanced ORM), along with powerful features such as calculated attributes or dataclass functions. It also provides access to data through entity and entity selection objects.

As a result, ORDA exposes the whole database as a set of data model objects, including model objects as well as data objects.

schema

Objects have Classes

With ORDA, you can declare and use high-level user class functions above the data model. This allows you to write business-oriented code and "publish" it just like an API. Datastore, dataclasses, entity selections, and entities are all available as class objects that can contain functions.

For example, you could create a getNextWithHigherSalary() function in the EmployeeEntity class to return employees with a salary higher than the selected one. It would be as simple as calling:

nextHigh=ds.Employee.get(1).getNextWithHigherSalary()

Thanks to this feature, the entire business logic of your Qodly application can be stored as a independent layer so that it can be easily maintained and reused with a high level of security:

  • You can "hide" the overall complexity of the underlying physical structure and only expose understandable and ready-to-use functions.
  • If the physical structure evolves, you can simply adapt function code and client applications will continue to call them transparently.
  • By default, all of your data model class functions (including calculated attribute functions) are not exposed to remote calls. You must explicitly declare each public function with the exposed keyword.

Class Architecture

ORDA provides generic classes exposed through a 4D class store, as well as specific user classes (extending generic classes) exposed in the cs class store:

All ORDA data model classes are exposed as properties of the cs class store. The following ORDA classes are available:

ClassExample nameInstantiated by
cs.DataStorecs.DataStoreds command
cs.DataClassNamecs.EmployeedataStore.DataClassName, dataStore["DataClassName"]
cs.DataClassNameEntitycs.EmployeeEntitydataClass.get(), dataClass.new(), entitySelection.first(), entitySelection.last(), entity.previous(), entity.next(), entity.first(), entity.last(), entity.clone()
cs.DataClassNameSelectioncs.EmployeeSelectiondataClass.query(), entitySelection.query(), dataClass.all(), dataClass.fromCollection(), dataClass.newSelection(), entitySelection.drop(), entity.getSelection(), entitySelection.and(), entitySelection.minus(), entitySelection.or(), entitySelection.orderBy(), entitySelection.orderByFormula(), entitySelection.slice()

Also, object instances from ORDA data model user classes benefit from their parent's properties and functions:

Datastore

The datastore is the interface object to a database. It builds a representation of the whole database as object. A datastore is made of a model and data:

  • The model contains and describes all the dataclasses that make up the datastore. It is independant from the underlying database itself.
  • Data refers to the information that is going to be used and stored in this model. For example, names, addresses, and birthdates of employees are pieces of data that you can work with in a datastore.

When handled through the code, the datastore is an object named DataStore, returned by the ds command, whose properties are all of the dataclasses which have been specifically exposed.

The DataStore object itself cannot be copied as an object:

mydatastore=objectCopy(ds) //returns null

The datastore properties are however enumerable:

 var names : collection
names=objectKeys(ds)
//names contains the names of all the dataclasses

DataStoreImplementation Class

A database exposes its own DataStoreImplementation class, named DataStore, in the cs class store.

  • Extends: 4D.DataStoreImplementation
  • Class name in cs class store: DataStore

You create functions in the DataStore class that will be available through the ds object, from any context of the application.

Example

// DataStore class

extends DataStoreImplementation

exposed function getDesc
return "Database exposing employees and their companies"

This function can then be called:

desc=ds.getDesc() //"Database exposing..."

Dataclass

A dataclass is the equivalent of a database table. It is used as an object model and references all fields as attributes, including relational attributes (attributes built upon relations between dataclasses) as well as calculated and alias attributes. Relational, computed and alias attributes can be used in queries like any other attribute.

All dataclasses in a Qodly project are available as a property of the ds datastore. The Expose as REST resource option must be selected at the model level for each dataclass that you want to be called from the Web.

For example, consider the following database table:

The Company dataclass is available in the ds datastore. You can write:

var compClass : cs.Company //declares a compClass object variable of the Company class
compClass=ds.Company //assigns the Company dataclass reference to compClass

The dataclass offers an abstraction of the physical database and allows handling a conceptual data model with specific features such as computed attributes or alias attributes.

A dataclass object can contain:

  • storage attributes
  • relation attributes
  • computed attributes
  • alias attributes
  • functions

The dataclass is the only means to query the datastore. A query is done from a single dataclass. Queries are built around attributes and relation attribute names of the dataclasses. So the relation attributes are the means to involve several linked dataclasses in a query.

The dataclass object itself cannot be copied as an object:

mydataclass=objectCopy(ds.Employee) //returns null

The dataclass properties are however enumerable:

 var names : collection
names=objectKeys(ds.Employee)
//names contains the names of all the dataclass attributes

DataClass Class

Each dataclass offers a DataClass class in the cs class store.

  • Extends: 4D.DataClass
  • Class name in cs class store: DataClassName
  • Example name: Employee

Example 1

// Company class


extends DataClass

// Returns companies whose revenue is over the average
// Returns an entity selection related to the Company DataClass

function getBestOnes() : cs.CompanySelection
sel=this.query("revenues >= :1",this.all().average("revenues"))
return sel

Then you can get an entity selection of the "best" companies by executing:

	var best : cs.CompanySelection
best=ds.Company.getBestOnes()
note

Example 2

Considering the following model (partial view):

Zipcodes are used as primary keys of the ZipCode table. The many-to-one relation attribute between cityID and ID is named city.

The City Class provides an API:

// City class

extends DataClass

exposed function getCityName(zipcode : integer) -> result : string
var zip : cs.ZipCodeEntity

zip=ds.ZipCode.get(zipcode)
result=""

if (zip!=null)
result=zip.city.name
end

The application can use the API to get the city matching a zip code:

city=ds.City.getCityName(zipcode)

Entity

An entity is the equivalent of a record. It is actually an object that references a record in the database. It can be seen as an instance of a dataclass, like a record of the table matching the dataclass.

However, an entity also contains data correlated to the datastore. The purpose of the entity is to manage data (create, read, update, delete). When an entity reference is obtained by means of an entity selection, it also retains information about the entity selection which allows iteration through the selection.

For example, to create an entity:

 var status : object
var employee : cs.EmployeeEntity //declares a variable of the EmployeeEntity class type

employee=ds.Employee.new()
employee.firstName="Mary"
employee.lastName="Smith"
status=employee.save()

The entity object itself cannot be copied as an object:

 myentity=objectCopy(ds.Employee.get(1)) //returns null

The entity properties are however enumerable:

 var names : collection
names=objectKeys(ds.Employee.get(1))
//names contains the names of all the entity attributes

Entity Class

Each dataclass offers an Entity class in the cs class store.

  • Extends: 4D.Entity
  • Class name in cs class store: DataClassNameEntity
  • Example name: CityEntity

Calculated attributes

Entity classes allow you to define calculated attributes using specific keywords:

  • function get attributeName
  • function set attributeName
  • function query attributeName
  • function orderBy attributeName

For more information, please refer to the Calculated attributes section below.

Example

// cs.CityEntity class

extends Entity

function getPopulation() -> result : integer
result=this.zips.sum("population")


function isBigCity() -> result : boolean
// The getPopulation() function is usable inside the class
result=this.getPopulation()>50000

Then you can call this code:

var city : cs.CityEntity
var message : string

city=ds.City.getCity("Caguas")

if (city.isBigCity())
message=city.name + " is a big city"
end

Entity selection

An entity selection is an object containing one or more reference(s) to entities belonging to the same dataclass. It is usually created as a result of a query or returned from a relation attribute. An entity selection can contain 0, 1 or X entities from the dataclass -- where X can represent the total number of entities contained in the dataclass.

Example:

var e : cs.EmployeeSelection //declares a e object variable of the EmployeeSelection class type
e=ds.Employee.all() //assigns the resulting entity selection reference to the e variable

The entity selection object itself cannot be copied as an object:

 myentitysel=objectCopy(ds.Employee.all()) //returns null

The entity selection properties are however enumerable:

 var names : collection
names=objectKeys(ds.Employee.all())
//names contains the names of the entity selection properties
//("length", "00", "01"...)

EntitySelection Class

Each dataclass offers an EntitySelection class in the cs class store.

  • Extends: 4D.EntitySelection
  • Class name in cs class store: DataClassNameSelection
  • Example name: EmployeeSelection

Example

// EmployeeSelection class


extends EntitySelection

//Extract the employees with a salary greater than the average from this entity selection

function withSalaryGreaterThanAverage() -> result : cs.EmployeeSelection
result=this.query("salary > :1",this.average("salary")).orderBy("salary")

Then you can get employees with a salary greater than the average in any entity selection by executing:

moreThanAvg=ds.Company.all().employees.withSalaryGreaterThanAverage()

Ordered or unordered entity selection

For optimization reasons, by default ORDA usually creates unordered entity selections, except when you call the orderBy() function or use specific options. In this documentation, unless specified, "entity selection" usually refers to an "unordered entity selection".

Ordered entity selections are created only when necessary or when specifically requested using options, i.e. in the following cases:

  • result of an orderBy() on a selection (of any type)
  • result of an orderByFormula() on a selection (of any type)
  • result of the newSelection() function with the dk keep ordered option
note

Ordered entity selections support duplicated entity references. On the other hand, when an ordered entity selection becomes an unordered entity selection, any repeated entity references are removed.

Unordered entity selections are created in all other cases, including:

  • result of a query() on a selection (of any type) or a query() on a dataclass,
  • result of a all(), fromCollection(), or newSelection() (without option) function on a dataclass,
  • result of various functions from the entity selection class, whatever the input selection types: or(), and(), add(), copy(), extract(), slice(), drop()...
  • result of a relation such as empSel=company.employees, or a projection such as empSel.name,
  • result of an entity.getSelection() function.

Attributes

There is no specific class for attribute objects. Basically, dataclass properties are attribute objects describing the underlying fields or relations. For example:

 var nameAttribute, revenuesAttribute : object
nameAttribute=ds.Company.name //reference to class attribute
revenuesAttribute=ds.Company["revenues"] //alternate way to reference

This code assigns to nameAttribute and revenuesAttribute references to the name and revenues attributes of the Company dataclass. This syntax does NOT return values held inside of the attribute, but instead returns objects describing the attributes themselves, that you can handle by calling the dataclass attribute name. To handle values, you need to go through Entities.

The Expose as REST resource option must be selected at the model level for each attribute that you want to be called from the Web (by default this option is inherited from the dataclass level).

Dataclass attributes come in several kinds: storage, relatedEntity, relatedEntities, computed (aka calculated), or alias. Attributes that are scalar (i.e., provide only a single value) support all the standard data types (integer, text, object, etc.).

Storage and Relation attributes

  • A storage attribute is equivalent to a field in a database and can be indexed. Values assigned to a storage attribute are stored as part of the entity when it is saved. When a storage attribute is accessed, its value comes directly from the datastore. Storage attributes are the most basic building block of an entity and are defined by name and data type.
  • A relation attribute provides access to other entities. Relation attributes can result in either a single entity (or no entity) or an entity selection (0 to N entities). Relation attributes are built upon "classic" relations in the relational structure to provide direct access to related entity or related entities. Relation attributes are directy available in ORDA using their names.

For example, consider the following partial model and the relation properties:

All storage attributes will be automatically available:

  • in the Project dataclass: "ID", "name", and "companyID"
  • in the Company dataclass: "ID", "name", and "discount"

In addition, the following relation attributes will also be automatically available:

  • in the Project dataclass: theClient attribute, of the "relatedEntity" kind; there is at most one Company for each Project (the client)
  • in the Company dataclass: companyProjects attribute, of the "relatedEntities" kind; for each Company there is any number of related Projects.

All dataclass attributes are exposed as properties of the dataclass:

	// Company available attributes
ds.Company.companyProjects //relatedEntities
ds.Company.discount
ds.Company.ID
ds.Company.name

//Project available attributes
ds.Project.companyID
ds.Project.ID
ds.Project.name
ds.Project.theClient //relatedEntity

Keep in mind that these objects describe attributes, but do not give access to data. Reading or writing data is done through entity objects.

Alias attributes

An alias attribute is built above another attribute of the data model, named target attribute. The target attribute can belong to a related dataclass (available through any number of relation levels) or to the same dataclass. An alias attribute stores no data, but the path to its target attribute. You can define as many alias attributes as you want in a dataclass.

Calculated attributes

Calculated attributes are declared using a get <attributeName> function in the Entity class definition. Their value is not stored but evaluated each time they are accessed. They do not belong to the underlying database structure, but are usually built upon it and can be used as any attribute of the data model.

info

ORDA calculated attributes are not exposed by default. You expose a calculated attribute by adding the exposed keyword to the get function definition.

function get <attributeName>

Syntax

{exposed} function get <attributeName>({event : object}) -> result : type
// code

The getter function is mandatory to declare the attributeName calculated attribute. Whenever the attributeName is accessed, the function get code is evaluated and the result value is returned.

A calculated attribute can use the value of other calculated attribute(s). Recursive calls generate errors.

The getter function defines the data type of the calculated attribute thanks to the result parameter. The following resulting types are allowed:

  • Scalar (string, boolean, date, time, number)
  • object
  • Image
  • BLOB
  • Entity (i.e. cs.EmployeeEntity)
  • Entity selection (i.e. cs.EmployeeSelection)

The event parameter contains the following properties:

PropertyTypeDescription
attributeNamestringCalculated attribute name
dataClassNamestringDataclass name
kindstring"get"
resultvariantOptional. Add this property with null value if you want a scalar attribute to return null

Examples

  • fullName calculated attribute:
function get fullName(event : object)-> fullName : string

switch
: (this.firstName==null) & (this.lastName==null)
event.result=null //use result to return null
: (this.firstName==null)
fullName=this.lastName
: (this.lastName==null)
fullName=this.firstName
else
fullName=this.firstName+" "+this.lastName
end
  • A calculated attribute can be based upon an entity related attribute:
function get bigBoss(event : object)-> result: cs.EmployeeEntity
result=this.manager.manager

  • A calculated attribute can be based upon an entity selection related attribute:
function get coWorkers(event : object)-> result: cs.EmployeeSelection
if (this.manager==null)
result=ds.Employee.newSelection()
else
result=this.manager.directReports.minus(this)
end

function set <attributeName>

Syntax

function set <attributeName>(value : type \{, event : object})
// code

The setter function executes whenever a value is assigned to the attribute. this function usually processes the input value(s) and the result is dispatched between one or more other attributes.

The value parameter receives the value assigned to the attribute.

The event parameter contains the following properties:

PropertyTypeDescription
attributeNamestringCalculated attribute name
dataClassNamestringDataclass name
kindstring"set"
valuevariantValue to be handled by the calculated attribute

Example

function set fullName(value : string , event : object)
var p : integer
p=position(" ",value)
this.firstname=substring(value, 1, p-1) // "" if p<0
this.lastname=substring(value, p+1)

function query <attributeName>

Syntax

function query <attributeName>(event : object)
function query <attributeName>(event : object) -> result : string
function query <attributeName>(event : object) -> result : object
// code

This function supports three syntaxes:

  • With the first syntax, you handle the whole query through the event.result object property.
  • With the second and third syntaxes, the function returns a value in result:
    • If result is a string, it must be a valid query string
    • If result is an object, it must contain two properties:
    PropertyTypeDescription
    result.querystringValid query string with placeholders (:1, :2, etc.)
    result.parameterscollectionvalues for placeholders

The query function executes whenever a query using the calculated attribute is launched. It is useful to customize and optimize queries by relying on indexed attributes. When the query function is not implemented for a calculated attribute, the search is always sequential (based upon the evaluation of all values using the get <AttributeName> function).

note

The following features are not supported:

  • calling a query function on calculated attributes of type Entity or Entity selection,
  • using the order by keyword in the resulting query string.

The event parameter contains the following properties:

PropertyTypeDescription
attributeNamestringCalculated attribute name
dataClassNamestringDataclass name
kindstring"query"
valuevariantValue to be handled by the calculated attribute
operatorstringQuery operator (see also the query class function). Possible values:
  • >;== (equal to, @ is wildcard)
  • === (equal to, @ is not wildcard)
  • != (not equal to, @ is wildcard)
  • !== (not equal to, @ is not wildcard)
  • < (less than)
  • <= (less than or equal to)
  • > (greater than)
  • >= (greater than or equal to)
  • IN (included in)
  • % (contains keyword)
  • resultvariantValue to be handled by the calculated attribute. Pass null in this property if you want to execute a default query (always sequential for calculated attributes).

    If the function returns a value in result and another value is assigned to the event.result property, the priority is given to event.result.

    Examples

    • Query on the fullName calculated attribute.
    function query fullName(event : object)->result : object

    var fullname, firstname, lastname, myQuery : string
    var operator, myQuery : string
    var p : integer
    var parameters : collection

    operator=event.operator
    fullname=event.value

    p=position(" ",fullname)
    if (p>0)
    firstname=substring(fullname, 1, p-1)+"@"
    lastname=substring(fullname, p+1)+"@"
    parameters=newCollection(firstname, lastname) // two items collection
    else
    fullname=fullname+"@"
    parameters=newCollection(fullname) // single item collection
    end

    switch
    : (operator=="==") | (operator=="===")
    if (p>0)
    myQuery="(firstName = :1 and lastName = :2) or (firstName = :2 and lastName = :1)"
    else
    myQuery="firstName = :1 or lastName = :1"
    end
    : (operator="!=")
    if (p>0)
    myQuery="firstName != :1 and lastName != :2 and firstName != :2 and lastName != :1"
    else
    myQuery="firstName != :1 and lastName != :1"
    end
    end

    result=newObject("query", myQuery, "parameters", parameters)

    Keep in mind that using placeholders in queries based upon user text input is recommended for security reasons (see query() description).

    Calling code, for example:

    emps=ds.Employee.query("fullName = :1", "Flora Pionsin")
    • This function handles queries on the age calculated attribute and returns an object with parameters:
    function query age(event : object)->result : object

    var operator, myQuery : string
    var age : integer

    operator=event.operator

    age=num(event.value) // integer
    d1=addToDate(currentDate, -age-1, 0, 0)
    d2=addToDate(d1, 1, 0, 0)
    parameters=newCollection(d1, d2)

    switch

    : (operator=="==")
    myQuery="birthday > :1 and birthday <= :2" // after d1 and before or egal d2

    : (operator=="===")

    myQuery="birthday = :2" // d2 = second calculated date (= birthday date)

    : (operator==">=")
    myQuery="birthday <= :2"

    //... other operators


    end


    if (undefined(event.result))
    result=newObject
    result.query=myQuery
    result.parameters=parameters
    end

    Calling code, for example:

    // people aged between 20 and 21 years (-1 day)
    twenty=people.query("age = 20") // calls the "==" case

    // people aged 20 years today
    twentyToday=people.query("age === 20") // equivalent to people.query("age is 20")

    function orderBy <attributeName>

    Syntax

    function orderBy <attributeName>(event : object)
    function orderBy <attributeName>(event : object)-> result : string

    // code

    The orderBy function executes whenever the calculated attribute needs to be ordered. It allows sorting the calculated attribute. For example, you can sort fullName on first names then last names, or conversely. When the orderBy function is not implemented for a calculated attribute, the sort is always sequential (based upon the evaluation of all values using the get <AttributeName> function).

    info

    Calling an orderBy function on calculated attributes of type Entity class or Entity selection class is not supported.

    The event parameter contains the following properties:

    PropertyTypeDescription
    attributeNamestringCalculated attribute name
    dataClassNamestringDataclass name
    kindstring"orderBy"
    valuevariantValue to be handled by the calculated attribute
    operatorstring"desc" or "asc" (default)
    descendingbooleantrue for descending order, false for ascending order
    resultvariantValue to be handled by the calculated attribute. Pass null if you want to let Qodly execute the default sort.

    You can use either the operator or the descending property. It is essentially a matter of programming style (see examples).

    You can return the orderBy string either in the event.result object property or in the result function result. If the function returns a value in result and another value is assigned to the event.result property, the priority is given to event.result.

    Example

    You can write conditional code:

    function orderBy fullName(event : object)-> result : string
    if (event.descending==true)
    result="firstName desc, lastName desc"
    else
    result="firstName, lastName"
    end

    You can also write compact code:

    function orderBy fullName(event : object)-> result : string
    result="firstName "+event.operator+", "lastName "+event.operator

    Conditional code is necessary in some cases:

    function orderBy age(event : object)-> result : string
    if (event.descending==true)
    result="birthday asc"
    else
    result="birthday desc"
    end

    Exposed vs non-exposed functions

    For security reasons, all of your data model class functions are not exposed (i.e., private) by default to web requests.

    A function that is not exposed is not available from web requests and cannot be called on any object instance. You define non-exposed functions (as well as dataclasses or attributes) if you want them to be only available on the server through local code. If a web request tries to access a non-exposed function, the "-10729 - Unknown member method" error is returned.

    To allow a data model class function to be called by a remote request, you must explicitly declare it using the exposed keyword. The formal syntax is:

    // declare an exposed function
    exposed function <functionName>
    note

    The exposed keyword can only be used with Data model class functions as well as shared singleton functions. If used with a regular user class function, it is ignored.

    Example

    You want an exposed function to use a private function in a dataclass class:

    extends DataClass

    //Public function
    exposed function registerNewStudent(student : object) -> status : object

    var entity : cs.StudentsEntity

    entity=ds.Students.new()
    entity.fromObject(student)
    entity.school=this.query("name=:1", student.schoolName).first()
    entity.idNumber=this.computeIDNumber()
    status=entity.save()

    //Not exposed (private) function
    function computeIDNumber()-> id : integer
    //compute a new ID number
    id=...

    When the code is called:

    var student , status : object
    var id : integer

    student=newObject("firstname", "Mary", "lastname", "Smith", "schoolName", "Math school")

    status=ds.Schools.registerNewStudent(student) //can be called from a web request
    // id=ds.Schools.computeIDNumber() // Error "Unknown member method" if called from a web request

    onHttpGet keyword

    Use the onHttpGet keyword to declare functions that can be called through HTTP requests using the GET verb. Such functions can return any web contents, for example using the 4D.OutgoingMessage class.

    The onHttpGet keyword is available with:

    The formal syntax is:

    // declare an onHttpGet function
    exposed onHttpGet function <functionName>(params) : result
    info

    The exposed keyword must also be added in this case, otherwise an error will be generated.

    caution

    As this type of call is an easy offered action, the developer must ensure no sensitive action is done in such functions.

    params

    A function with onHttpGet keyword accepts parameters.

    In the HTTP GET request, parameters must be passed directly in the URL and declared using the $params keyword (they must be enclosed in a collection).

    IP:port/rest/<dataclass>/functionName?$params='[<params>]'

    See the Parameters section in the REST API documentation.

    result

    A function with onHttpGet keyword can return any value of a supported type (same as for REST parameters).

    info

    You can return a value of the 4D.OutgoingMessage class type to benefit from properties and functions to set the header, the body, and the status of the answer.

    Example

    You have defined the following function:


    extends DataClass

    exposed onHTTPGet function getThumbnail(name : string, width : integer, height : integer) : 4D.OutgoingMessage

    var fileRef = file("/SOURCES/Shared/Images/"+name+".jpg")
    var blob = fileRef.getContent()
    blobToPicture(blob,image)

    var image, thumbnail : picture
    var response = 4D.OutgoingMessage.new()

    createThumbnail(image, thumbnail, width, height, kScaledToFit)
    response.setBody(image)
    response.setHeader("Content-Type", "image/jpeg")
    return response

    It can be called by the following HTTP GET request:

    IP:port/rest/Products/getThumbnail?$params='["Yellow Pack",200,200]'