Relations (Retrieve)

Top  Previous  Next

When a data object or a collection of objects is retrieved using the client API, Backendless does not automatically include related objects into the returned object hierarchy. The reason for this is it would make it very easy to load a lot of unnecessary data which could impact application's performance. There are multiple ways to retrieve related data:

Relations auto-load - a mechanism built-into Backendless Console for configuring specific relations to be included into responses with the parent objects.
Single-step relations retrieval - initializing and including related objects into the response when running a find query for parent objects.
Two-step relations retrieval - a process of retrieving relations where the first step is to load the parent object and the second step is to load specific relations for the given parent.
Loading with relations depth - retrieving related objects by specifying the depth of the object hierarchy
Loading related child objects by condition applied to parent - load related objects using a search query where the condition applies to the parent object properties.

Relations Auto Load

By default when an object is retrieved from Backendless using any of the find APIs (basic or advanced), its related objects are not included into the response, unless explicitly referenced in the request. This behavior can be easily modified using Backendless Console:

auto-load-checkbox.zoom60

For any two tables A and B where A has a relationship column linking it to B, the console includes the "auto load" checkbox for the relationship column. Selecting the checkbox instructs Backendless to return all related B objects when the parent instance of A is retrieved through an API call. For example, in the image above, the Order table has the one-to-many "items" relationship with the OrderItem table. When the "auto load" checkbox in the "items" column is selected, all related OrderItem objects will be included into the response for a find query for the Order table.

Single Step Retrieval

This approach allows to retrieve related objects along with the parent object in a single find request. Each relationship property (column) must be uniquely identified by name using the following API:

BackendlessDataQuery query = new BackendlessDataQuery();
QueryOptions queryOptions = new QueryOptions();
queryOptions.addRelated( "RELATED-PROPERTY-NAME" );
queryOptions.addRelated( "RELATED-PROPERTY-NAME.RELATION-OF-RELATION" );
query.setQueryOptions( queryOptions );

Then load data using the constructed query object with:

Synchronous call:
BackendlessCollection<Map> collection = Backendless.Data.of( "TABLE-NAME" ).find( query );
Asynchronous call:
Backendless.Data.of( "TABLE-NAME" ).find( query, AsyncCallback<BackendlessCollection<Map>> callback );
Synchronous call:
BackendlessCollection<T> collection = Backendless.Data.of( T.class ).find( query );
Asynchronous call:
Backendless.Data.of( T.class ).find( query, AsyncCallback<BackendlessCollection<T>> callback );

where

RELATED-PROPERTY-NAME- name of a related property to load. For example, if table Person, has a relation "homeAddress" pointing to an object in the Address table, the value would be "homeAddress". The syntax allows to add relations of relations. For example, if the same Address table has a relation "country" pointing to the Country table, then "homeAddress.country" would instruct the related Country object to be loaded as well.
T.class- Reference to a class which identifies the table from which the data is to be loaded.

Two Step Retrieval

With this approach a parent object is loaded first and then there is a separate API call to load specific related objects into the given parent object. For example, suppose the Order table has a one-to-many relationship column "items" pointing to the OrderItem table. The OrderItem table has a one-to-one relationship to the Manufacturer table, the code below loads first Order object and then loads related OrderItems and corresponding Manufacturers:

ArrayList<String> relationProps = new ArrayList<String>();
relationProps.add( "items" );
relationProps.add( "items.manufacturer" );

See the two-step retrieval process in the example below. Notice that the result of the loadRelations call the firstOrder object is populated with the related OrderItem objects:

Synchronous call:
// first step - retrieve object itself
Map firstOrder = Backendless.Data.of( "Order" ).findFirst();

// second step - retrieve relations
Backendless.Data.of( "Order" ).loadRelations( firstOrder, relationProps );
Asynchronous call:
// first step - retrieve object itself
Backendless.Data.of( "Order" ).findFirst(new AsyncCallback<Map>()
{
  @Override
  public void handleResponse( Map firstObject )
  {
     // second step - retrieve relations
     Backendless.Data.of( "Order" ).loadRelations( firstOrder, relationProps, 
                               new AsyncCallback<Map>>()
     {
       @Override
       public void handleResponse( <Map> response )
       {

       }

       @Override
       public void handleFault( BackendlessFault fault )
       {

       }
    });     
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {

  }
});
Synchronous call:
// first step - retrieve the object
Order firstOrder = Backendless.Data.of( Order.class ).findFirst();

// second step - retrieve relations
Backendless.Data.of( Order.class ).loadRelations( firstOrder, relationProps );
Asynchronous call:
// first step - retrieve object
Backendless.Data.of( Order.class ).findFirst( new AsyncCallback<Order>()
{
  @Override
  public void handleResponse( Order response )
  {
     // second step - retrieve relations
     Backendless.Data.of( Order.class ).loadRelations( firstOrder, relationProps, 
                               new AsyncCallback<Order>()
    {
      @Override
      public void handleResponse( Order response )
      {

      }

      @Override
      public void handleFault( BackendlessFault fault )
      {

      }
    });
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {

  }
});

Loading with Relations Depth

The Data Service API supports a mechanism for loading related objects without identifying each by its name. Instead, the API includes a parameter which specifies the "depth" of the relations to include into the response. Consider the following diagram:

relation-depth

The diagram shows a hierarchy for class structure - the Order class has two relations: with OrderItem and Customer classes. Each in turn has a relation to the Manufacturer and Address classes. When an instance or a collection of Order objects is retrieved from Backendless, the API may include a parameter specifying the depth of relations to include into the response. If the relation depth is 1, then all related instances of OrderItem and Customer will be included into each Order object. If the relation depth is 2, then not only OrderItem and Customer instances will be included, but the corresponding Manufacturer and Address objects as well.

API methods supporting relations depth

Synchronous methods:
public Map Backendless.Data.of( "TABLE-NAME" ).findFirst( int relationsDepth );
public Map Backendless.Data.of( "TABLE-NAME" ).findLast( int relationsDepth );
public Map Backendless.Data.of( "TABLE-NAME" ).findById( String objectId, int relationsDepth );
public BackendlessCollection<Map> Backendless.Data.of( "TABLE-NAME" ).find( BackendlessDataQuery dataQuery );
Asynchronous methods:
public void Backendless.Data.of( "TABLE-NAME" ).findFirst( int relationsDepth,
                                                      AsyncCallback<Map> responder  );
public void Backendless.Data.of( "TABLE-NAME" ).findLast( int relationsDepth, 
                                                     AsyncCallback<Map> responder  );
public void Backendless.Data.of( "TABLE-NAME" ).findById( String objectId, int relationsDepth, 
                                                     AsyncCallback<Map> responder  );
public void Backendless.Data.of( "TABLE-NAME" ).find( BackendlessDataQuery dataQuery, 
                                                 AsyncCallback<BackendlessCollection<Map>> responder );
Synchronous methods:
public T Backendless.Data.of( T.class ).findFirst( int relationsDepth );
public T Backendless.Data.of( T.class ).findLast( int relationsDepth );
public T Backendless.Data.of( T.class ).findById( String objectId, int relationsDepth );
public BackendlessCollection<T> Backendless.Data.of( T.class ).find( BackendlessDataQuery dataQuery );
Asynchronous methods:
public void Backendless.Data.of( T.class ).findFirst( int relationsDepth,
                                                      AsyncCallback<E> responder  );
public void Backendless.Data.of( T.class ).findLast( int relationsDepth, 
                                                     AsyncCallback<E> responder  );
public void Backendless.Data.of( T.class ).findById( String objectId, int relationsDepth, 
                                                     AsyncCallback<E> responder  );
public void Backendless.Data.of( T.class ).find( BackendlessDataQuery dataQuery, 
                                                 AsyncCallback<BackendlessCollection<T>> responder );

Example:

BackendlessDataQuery query = new BackendlessDataQuery();
QueryOptions queryOptions = new QueryOptions();
queryOptions.relationsDepth = 2;
query.setQueryOptions( queryOptions );

// synchronous call
BackendlessCollection<Map> collection = Backendless.Data.of( "Foo" ).find( query );

// asynchronous call
Backendless.Data.of( "Foo" ).find( query, 
                            new AsyncCallback<BackendlessCollection<Map>>()
    {
      @Override
      public void handleResponse( BackendlessCollection<Map> response )
      {

      }

      @Override
      public void handleFault( BackendlessFault fault )
      {

      }
    });
BackendlessDataQuery query = new BackendlessDataQuery();
QueryOptions queryOptions = new QueryOptions();
queryOptions.relationsDepth = 2;
query.setQueryOptions( queryOptions );

// synchronous call
BackendlessCollection<Foo> collection = Backendless.Data.of( Foo.class ).find( query );

// asynchronous call
Backendless.Data.of( Foo.class ).find( dataQuery, 
                   new AsyncCallback<BackendlessCollection<Foo>>()
{
  @Override
  public void handleResponse( BackendlessCollection<Foo> response )
  {

  }

  @Override
  public void handleFault( BackendlessFault fault )
  {

  }
});

Loading a Subset of Related Child Objects

Backendless supports a special query syntax for loading a subset of child objects for a specific parent. Consider the following class diagram:

relational-data-example-uml

Notice the PhoneBook entity references Contact through two properties - "owner" (the one to one relationship) and "contacts" (the one to many relationship). Each contact entity also references Address. These entities will be used to demonstrate how Backendless handles related entities in all basic data persistence operations. Consider the following class definitions for these entities:

The Map-based approach does not require you defining classes for the objects stored in Backendless. Instead your code can use java.util.Map to to store and retrieve objects in/from Backendless.
Consider the following class definitions for the entities from the diagram: Person class:
package com.sample.entity;

import java.util.List;

public class PhoneBook
{
  private String objectId;
  private Contact owner;
  private List<Contact> contacts;

  public String getObjectId()
  {
    return objectId;
  }

  public void setObjectId( String objectId )
  {
    this.objectId = objectId;
  }

  public Contact getOwner()
  {
    return owner;
  }

  public void setOwner( Contact owner )
  {
    this.owner = owner;
  }

  public List<Contact> getContacts()
  {
    return contacts;
  }

  public void setContacts( List<Contact> contacts )
  {
    this.contacts = contacts;
  }
}
Contact class:
package com.sample.entity;

import java.util.Date;

public class Contact
{
  private String objectId;
  private String name;
  private int age;
  private String phone;
  private String title;
  private Address address;
  private Date updated;

  public String getObjectId()
  {
    return objectId;
  }

  public void setObjectId( String objectId )
  {
    this.objectId = objectId;
  }

  public String getName()
  {
    return name;
  }

  public void setName( String name )
  {
    this.name = name;
  }

  public int getAge()
  {
    return age;
  }

  public void setAge( int age )
  {
    this.age = age;
  }

  public String getPhone()
  {
    return phone;
  }

  public void setPhone( String phone )
  {
    this.phone = phone;
  }

  public String getTitle()
  {
    return title;
  }

  public void setTitle( String title )
  {
    this.title = title;
  }

  public Address getAddress()
  {
    return address;
  }

  public void setAddress( Address address )
  {
    this.address = address;
  }

  public Date getUpdated()
  {
    return updated;
  }

  public void setUpdated( Date updated )
  {
    this.updated = updated;
  }
}
Address class:
package com.sample.entity;

public class Address
{
  private String street;
  private String city;
  private String state;

  public String getStreet()
  {
    return street;
  }

  public void setStreet( String street )
  {
    this.street = street;
  }

  public String getCity()
  {
    return city;
  }

  public void setCity( String city )
  {
    this.city = city;
  }

  public String getState()
  {
    return state;
  }

  public void setState( String state )
  {
    this.state = state;
  }
}

 

The general structure of the query to load a collection of child objects for a specific parent object is:

ParentTableName[ relatedPropertyForChildrenCollection ].parentColumnName COLUMN-VALUE-CONDITION

A query in this format fetches a collection of child objects from a table which the relatedPropertyForChildrenCollection column points to.  The examples below demonstrate the usage of this syntax:

Find all contacts in a city for a specific phone book:

// use a sample from the "Relations (Save/Update) section 
// where PhoneBook is created with multiple contacts,
// then use the savedPhoneBook instance:
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.get( "objectId" ).append( "'" );
whereClause.append( " and " );
whereClause.append( "address.city = 'Smallville'" );

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( whereClause.toString() );
List<Map> result;
result = Backendless.Persistence.of( "Contact" ).find( dataQuery ).getCurrentPage();
// use a sample from the "Relations (Save/Update) section 
// where PhoneBook is created with multiple contacts,
// then use the savedPhoneBook instance:
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.getObjectId() ).append( "'" );
whereClause.append( " and " );
whereClause.append( "address.city = 'Smallville'" );

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( whereClause.toString() );
List<Contact> result;
result = Backendless.Persistence.of( Contact.class ).find( dataQuery ).getCurrentPage();

Find all contacts for the specific phone book where the city name contains letter 'a':

// use a sample from the "Relations (Save/Update) section 
// where PhoneBook is created with multiple contacts,
// then use the savedPhoneBook instance:
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.get( "objectId" ) ).append( "'" );
whereClause.append( " and " );
whereClause.append( "address.city like '%a%'" );

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( whereClause.toString() );
List<Map> result;
result = Backendless.Persistence.of( "Contact" ).find( dataQuery ).getCurrentPage();
// use a sample from the "Relations (Save/Update) section 
// where PhoneBook is created with multiple contacts,
// then use the savedPhoneBook instance:
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.getObjectId() ).append( "'" );
whereClause.append( " and " );
whereClause.append( "address.city like '%a%'" );

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( whereClause.toString() );
List<Contact> result;
result = Backendless.Persistence.of( Contact.class ).find( dataQuery ).getCurrentPage();

Find all contacts where age is greater than 20 for a specific phone book:

// use a sample from the "Relations (Save/Update) section 
// where PhoneBook is created with multiple contacts,
// then use the savedPhoneBook instance:
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.get( "objectId" ) ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age > 20" );

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( whereClause.toString() );
List<Map> result;
result = Backendless.Persistence.of( "Contact" ).find( dataQuery ).getCurrentPage();
// use a sample from the "Relations (Save/Update) section 
// where PhoneBook is created with multiple contacts,
// then use the savedPhoneBook instance:
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.getObjectId() ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age > 20" );

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( whereClause.toString() );
List<Contact> result;
result = Backendless.Persistence.of( Contact.class ).find( dataQuery ).getCurrentPage();

Find all contacts for a specific phone book where age is within the specified range:

// use a sample from the "Relations (Save/Update) section 
// where PhoneBook is created with multiple contacts,
// then use the savedPhoneBook instance:
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.get( "objectId" ) ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age >= 21 and age <= 30" )

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( whereClause.toString() );
List<Map> result;
result = Backendless.Persistence.of( "Contact" ).find( dataQuery ).getCurrentPage();
// use a sample from the "Relations (Save/Update) section 
// where PhoneBook is created with multiple contacts,
// then use the savedPhoneBook instance:
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.getObjectId() ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age >= 21 and age <= 30" )

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( whereClause.toString() );
List<Contact> result;
result = Backendless.Persistence.of( Contact.class ).find( dataQuery ).getCurrentPage();

Find all contacts for a specific phone book where age is greater than 20 and the city is Tokyo:

// use a sample from the "Relations (Save/Update) section 
// where PhoneBook is created with multiple contacts,
// then use the savedPhoneBook instance:
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.get( "objectId" ) ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age > 20 and address.city = 'Tokyo'" );

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( whereClause.toString() );
List<Map> result;
result = Backendless.Persistence.of( "Contact" ).find( dataQuery ).getCurrentPage();
// use a sample from the "Relations (Save/Update) section 
// where PhoneBook is created with multiple contacts,
// then use the savedPhoneBook instance:
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.getObjectId() ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age > 20 and address.city = 'Tokyo'" );

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( whereClause.toString() );
List<Contact> result;
result = Backendless.Persistence.of( Contact.class ).find( dataQuery ).getCurrentPage();

Please let us know how we can improve the documentation by leaving a comment. All technical questions should be posted to the Backendless Support forum. We do not respond to the technical questions on the documentation pages.: