Using Query Variables (Qn) in Query Expressions

An Efficient Method for Dividing Queries into Smaller Parts

Query variables (Qn) provide the well-known 'Divide and Conquer' principle for database query programmers, introducing a new feature for query expressions.

  • RQL follows the principles of object-oriented programming (OOP), allowing complex search conditions to be written in a single expression.
  • Query variables (Qn) can be used to split a long search condition into multiple separate expressions, making it easier to manage and understand.
  • The number of parts the search expression is divided into depends on the programmer's preferences, as dividing into parts is typically not mandatory.
  • Processing and refining the results of query variables (Qn) is supported in various ways.
The object model shown in the image below represents a Person class with relationships to Organization, Car, and recursively to itself through the friends field. The Car class has two subclasses, WagonCar and SuperCar, and a relationship to ServiceEvent, which in turn relates to ServiceType. Creating a search condition for Person objects using all three of its relationships - organization, cars, and friends - provides an excellent opportunity to apply and demonstrate the use of query variables.
RootDB IDatabase

In the source code below, the search conditions for the database query are divided into five query variables: Q1 to Q5. The search variable Q1 is executed by calling query.queryQ1Objects(), which returns the Person objects found in the database search. In Q1, search conditions are set for three fields of the Person class (organization, cars, and friends), each referencing one of the query variables Q2, Q3, and Q4. The IN operator specifies that the accepted Person objects must reference at least one object from the result set of the query variable on the right-hand side of the operand.

The query variable Q2 specifies that the Organization object must have a name that is either 'organization1' or 'organization2'.

The method call addQ3Query contains the search conditions for the query variable Q3, which is defined for the Car class. The Car class has two subclasses, WagonCar and SuperCar, and the condition on the modelYear field restricts the accepted Car objects to the SuperCar type. The SuperCar class is specified by its canonical name fi.rootrql.example1.SuperCar. Additionally, the modelYear field must be either 1976 or within the range of 1971 to 1974. Note that the list of values can include ranges, specified as either inclusive or exclusive using brackets - '(' for exclusive and ']' for inclusive. In this case, 1970 is exclusive, while 1974 is inclusive. Furthermore, the search requires that the car's service history includes a ServiceEvent whose ServiceType is present in the result set of Q5.

The method call addQ4Query creates an interesting search condition, as it defines a recursive relationship within the Person class—searching from Person to Person. The condition specifies that the accepted Person objects must have at least one friend (also a Person object) whose hobby is either slalom or rally.

Finally, the last query variable, Q5, defines the search condition for ServiceType class objects. The serviceCode field of the ServiceType object must contain the value '001-1001'.

RootStore r1 = rdb.getRootStore("fi.rootrql.example1.Person");
RootStore r2 = rdb.getRootStore("fi.rootrql.example1.Car");                  
RootStore r3 = rdb.getRootStore("fi.rootrql.example1.Organization");                  
RootStore r4 = rdb.getRootStore("fi.rootrql.example1.ServiceType");                  
Query query = Query.createQuery()
.addQ1Query(r1, "(organization IN (Q2), cars IN (Q3), friends IN (Q4))")
.addQ2Query(r3, "(name = 'organization1') OR (name = 'organization2')")
.addQ3Query(r2, "(modelYear (fi.rootrql.example1.SuperCar) IN "
                + "(1976, (1970,  1974]),"
                + " serviceEvents.serviceType IN (Q5))")
.addQ4Query(r1, "(friends.hobby IN ('slalom','rally'))")
.addQ5Query(r4, "(serviceCode = '001-1001')")
.setParts(true);
List<Person> list = query.queryQ1Objects();

The five query variables, Q1 to Q5, form a hierarchy illustrated in the image below. Q1 is the root query variable, and its execution is initiated by the Query object's method call queryQ1Objects(). A database query can have only one root query variable. In this case, executing Q1 requires that the results of query variables Q2, Q3, and Q4 are resolved first. Additionally, the search condition for query variable Q3 refers to Q5, which must be executed before Q3. In this way, the hierarchy of query variables defines their execution order.

Note that both direct and indirect recursion are not allowed in the query variable hierarchy. However, multiple parent relationships are supported, meaning a query variable can be used more than once in the search conditions of other query variables. Additionally, it is permissible to refer to the same query variable multiple times within a single search condition of another query variable.

RootDB IDatabase

The database query source code above is straightforward and intuitive for programmers to write, as its conceptual level follows Object-Oriented Programming (OOP) principles. The programmer does not need to understand the underlying persistence structures or database schema. In fact, with object databases, programmers are usually unaware of the database's storage structures (schema).

One Pure OOP Expression as an Alternative to Multiple Query Variables in Query Expressions

The use of query variables is a matter of preference

Although the above query with multiple query variables adheres to the OOP paradigm, it can be refactored into a single query expression that more strongly emphasizes OOP principles. In this refactored version, all search conditions are defined relative to the Person class, requiring the use of OOP dot notation multiple times to specify the search condition fields. As a result, dot notation is used more frequently than in the previous version with multiple query variables.

Limiting accepted Car objects to SuperCar instances requires listing the class canonical names from the Person class to its cars field, where the restriction is applied. Additionally, the accepted classes must also be constrained in the serviceCode field, which is referenced as cars.serviceEvents.serviceType.serviceCode. It is important to note that the classes are listed from the Person class to the Car class, rather than extending to the full chain of field names. This is because classes only need to be specified up to the point where ambiguity is eliminated. Since both ServiceEvent and ServiceType are classes without inheritance, there are no alternative classes, and thus, listing them further is unnecessary.

The source code shown below also includes the use of the OR and MERGE operators, which are not needed in the multiple query variables version described above.

RootStore r1 = rdb.getRootStore("fi.rootrql.example1.Person");
.addQ1Query(r1, "(((organization.name = 'organization1') OR"
    +           "  (organization.name = 'organization2')) MERGE"
    +  "(friends.hobby IN ('slalom','rally') ,"
    +  "cars.modelYear (fi.rootrql.example1.Person,"
    +  " fi.rootrql.example1.SuperCar) IN (1976, (1970,1974]),"
    +  "cars.serviceEvents.serviceType.serviceCode "
    +  "(fi.rootrql.example1.Person, fi.rootrql.example1.SuperCar) ="
    +  "'001-1001'))")
.setParts(true);
List<Person> list = query.queryQ1Objects();

Setting the parts attribute to true means that only the data structures from the Person root objects to the objects matching the search conditions are returned as search results. If the parts attribute is set to its default value of false, the complete data structures are returned. It's important to note that an object's data structure can contain a large number of objects, but typically, only a subset is needed. In fact, it is often rare to require all the objects within an object's full data structure. RQL provides other sophisticated methods to control which parts of data structures are included in the result objects, which can be particularly beneficial when handling large data structures.

Grouping search conditions using query variables (Qn) can simplify the writing and understanding of complex search conditions. On the other hand, using only one query variable emphasizes object-oriented programming (OOP) principles in an interesting way. It is also possible to combine these two methods, creating a hybrid approach that leverages the advantages of both. The choice of method for writing search conditions has a relatively small impact on database query execution time. However, when the same query variable appears multiple times in the search criteria, avoiding its use as a query variable can increase execution time, as the same search conditions would be executed repeatedly.

Operator Examples for Processing Query Variable Results

Operators for Getting Specific Objects or Extending Search Results

RQL provides several operators for query variables (Qn), including the GET and EXTEND operators, which are briefly introduced here.

The results of database searches are objects whose data structures typically contain objects from multiple classes. It is always possible to access other objects within the result data structure through the returned root objects. However, in many cases, the relevant portion of the result data structure may be a specific object type or its associated data structure. The GET operator in RQL is specifically used for this purpose.

The query conditions for the query variable Q2 below search for friends of Person objects who play golf and own at least one car of the SuperCar type. The notation for the query expression is largely self-explanatory, as it follows OOP principles, but the AND++ operator requires further explanation.

RQL offers a wide range of logical operators specifically designed for object data structures. The simple AND operator computes the mathematical intersection of two data structures, exactly as understood in mathematics. The extended AND++ operator, used below, modifies this behavior by adding objects from the right-hand operand if they completely overlap with the mathematical result in their initial part. It's important to note that the condition (friends.cars CLASS fi.rootrql.example1.SuperCar) also returns SuperCar cars of friends who do not play golf. The overlap requirement in AND++ filters out these unwanted SuperCar cars by ensuring that only the objects fully matching the search conditions are included.

RootStore r1 = rdb.getRootStore("fi.rootrql.example1.Person");
Query query = Query.createQuery()
    .addQ1Query(r1, "Q2 GET friends.cars")
    .addQ2Query(r1, "((friends.hobby = 'golf') AND++ "
                    "(friends.cars CLASS fi.rootrql.example1.SuperCar))")
    .setParts(true);
List<Car> list = query.queryQ1Objects();

"In the query expression for query variable Q1, the GET operator is used to return all Car objects located at friends.cars from the results of query variable Q2. In practice, Car objects are collected from the data structures of all returned root Person objects at friends.cars. The returned objects are Car objects, but in this case, they are all SuperCar objects, as only those were queried. The resulting list is a distinct list of Car objects, meaning that each car appears only once, even if it appears multiple times in the results of query variable Q2."

When the parts attribute is set to true (via the method call setParts(true)), the query results include only those objects specified in the search conditions. This is evident in the image below, which shows the list of SuperCar cars found. In the debug view, the fields of the first SuperCar are displayed, and all atomic fields have values. However, the list of service events (the serviceEvents field, which holds ServiceEvent objects) is empty (null). The serviceEvents field is empty because no search condition has been set for the ServiceEvent objects, nor is there a condition for objects in their associated data structures.

getSuperCarList-120

In the context of database searches, it is highly efficient to process only the data specified in the search conditions during execution. However, it is equally important that all other relevant data, such as the associated data structures of the result objects, can be easily retrieved. The EXTEND operator provides a convenient and easy to use solution for this purpose. A query variable can be added to the existing query to extend the SuperCar objects with their associated service events, as shown in the source code below.

RootStore r1 = rdb.getRootStore("fi.rootrql.example1.Person");
RootStore r2 = rdb.getRootStore("fi.rootrql.example1.Car");                        
Query query = Query.createQuery()
	.addQ1Query(r2, "Q2 EXTEND serviceEvents.serviceType")
    .addQ2Query(r1, "Q3 GET friends.cars")
    .addQ3Query(r1, "((friends.hobby = 'golf') AND++ "
                    "(friends.cars CLASS fi.rootrql.example1.SuperCar))")
    .setParts(true);
List<Car> list = query.queryQ1Objects();

The right-hand operand of the EXTEND operator defines the sequence of fields (serviceEvents.serviceType) to be included in the query result objects of its left-hand operand (query variable Q2). Note that Q2 contains SuperCar objects, which inherit from the root class Car, and thus the RootStore object r2 of the Car class is used. The EXTEND operator augments the SuperCar objects in Q2 with the associated service event objects, which are retrieved from the database according to the specified sequence of fields. As a result, the SuperCar objects now include all service events, as shown in the image below.

extendSuperCarList-120

In this case, it is also possible to obtain the same extended search result more simply and without using the EXTEND operator, because the search result Q1 is not used in any other search expression. By setting the parts attribute to false, all objects belonging to the data structures of the result objects are automatically retrieved from the database and included in the result. Since false is the default value for the parts attribute, it does not need to be explicitly set. The source code without the EXTEND operator is shown below.

RootStore r1 = rdb.getRootStore("fi.rootrql.example1.Person");
RootStore r2 = rdb.getRootStore("fi.rootrql.example1.Car");                        
Query query = Query.createQuery()
	.addQ1Query(r1, "Q2 GET friends.cars")
    .addQ2Query(r1, "((friends.hobby = 'golf') AND++ "
                    "(friends.cars CLASS fi.rootrql.example1.SuperCar))")
    .setParts(false);
List<Car> list = query.queryQ1Objects();

If Car objects had reference fields other than serviceEvents, all of their referenced objects would also be automatically retrieved from the database in the query result. This is not always desirable, which is why the EXTEND operator can be highly useful. It allows you to control and limit the extent to which reference field objects are retrieved from the database. For example, the search results can be limited to include only ServiceEvent objects, so that ServiceType objects in their serviceType reference fields are not retrieved or included in the results. The source code for this query is shown below.

RootStore r1 = rdb.getRootStore("fi.rootrql.example1.Person");
RootStore r2 = rdb.getRootStore("fi.rootrql.example1.Car");                        
Query query = Query.createQuery()
	.addQ1Query(r2, "Q2 EXTEND serviceEvents")
	.addQ2Query(r1, "Q3 GET friends.cars")
    .addQ3Query(r1, "((friends.hobby = 'golf') AND++ "
                    "(friends.cars CLASS fi.rootrql.example1.SuperCar))")
    .setParts(true);
List<Car> list = query.queryQ1Objects();

Now the SuperCar objects contain the service events (ServiceEvent objects), but their serviceType reference fields are empty (null), as shown in the image below.

extendLimitedSuperCarList-120

Recursion is also supported, which is a common practice in RQL. For example, the source code below searches for a Person object with the name p11, and its result in Q3 is extended to include objects in the fields friends.friends.cars. The sequence friends.friends.cars involves recursion, as the friends field is used twice. Finally, the same sequence of fields is used with the GET operator to retrieve the Car objects from Q2, which represents the extended data structure that also contains cars.

RootStore r1 = rdb.getRootStore("fi.rootrql.example1.Person");
Query query = Query.createQuery()
                .addQ1Query(r1, "Q2 GET friends.friends.cars")
                .addQ2Query(r1, "Q3 EXTEND friends.friends.cars")
                .addQ3Query(r1, "(name = 'p11')")
                .setParts(true);
List<Car> list = query.queryQ1Objects();

The EXTEND operator must be used, even when setParts(false) is applied, which returns the complete data structures of the result objects. Below is an example of an incorrect attempt to query Car objects. This query returns an empty result because RQL operates on a lazy principle, meaning only the data explicitly specified in the queries are retrieved from the database - nothing extra.

In this case, the query part for Q2 returns only the data identifying the Person object named 'p11', which does not include any information about other objects in its data structure. The GET operator does not retrieve data directly from the database; it operates only on the data structure that has already been fetched. Since Q2 contains only the data for the p11 object and no data about its friends, for example, the GET operator cannot find anything in the given field path and returns an empty result.

It's important to note that the setParts(false) setting only takes effect in the final step, when the query result is being prepared for return. At that point, based on the objects in the search result, all objects reachable from them are collected, and the complete data structures are returned. This approach ensures optimal database search performance.

RootStore r1 = rdb.getRootStore("fi.rootrql.example1.Person");
RootStore r2 = rdb.getRootStore("fi.rootrql.example1.Car");                        
Query query = Query.createQuery()
				.addQ1Query(r1, "Q2 GET friends.friends.cars")
				.addQ2Query(r1, "(name = 'p11')")
				.setParts(false); //also default value
List<Car> list = query.queryQ1Objects();

The methods described above allow for some adjustment of the objects included in the data structures of the search results. In addition to these, there are more advanced and precise ways to define the content of database search results. These methods enable the efficient selection of specific parts of data structures to be included or excluded from the results. Furthermore, the class inheritance hierarchy can be utilized to define the accepted object types within the data structures of the search results.

Links to other pages on this site.


Page content © 2024 

company name

Contact us:

mail address