Queries in the version 2.5 of jAgE place emphasis on providing a simple yet extensible API. They are a mechanism to perform a component introspection on individual objects and their collections.
How to use built-in query API
The system provides two classes (and few specialised subclasses) that offer a semi-fluent interface that allows to build queries in a readable way. These classes are: GenericQuery
for introspecting a single objects and MultiElementQuery
that allows to analyse full collections. Any query can bu built using them. However, following specialised cases are already included in the platform:
CollectionQuery
for querying implementations ofjava.util.Collection
interface.MapQuery
for querying maps (java.util.Map
).PropertyContainerCollectionQuery
for collections containing only property containers.AgentEnvironmentQuery
for querying an agent environment.
Introspecting components
It is possible to analyse the object (that follows JavaBeans conventions) using the GenericQuery
class.
The following example extracts the value of the someIntField
field.
QueriedObject target = new QueriedObject(); GenericQuery<QueriedObject, Integer> query = new GenericQuery<QueriedObject, Integer>(QueriedObject.class, Integer.class); query.select("someIntField"); Integer result = query.execute(target);
Querying collections
To query instances of Java collections (subclasses of the java.util.Collection
class) you can use the CollectionQuery
class. It is parametrised with the type of values located in the queried collection, i.e. when querying a List<MyClass>
instance, the CollectionQuery<MyClass>
should be used.
The following example shows how to select from the first 10 strings in the given list all strings that matches a regex li.*
or lorem[Ii]psum
.
List<String> testList = ...; CollectionQuery<String, String> q = new CollectionQuery<String, String>(String.class); q.from(first(10)).matching(anyOf(pattern("li.*"), pattern("lorem[Ii]psum"))); Collection<String> results = q.execute(testList);
Querying maps
To query instances of Java maps (subclasses of the java.util.Map
class) you can use the MapQuery
class. It is parametrised with types of keys and values located in the queried map.
The following example shows how to select from the map of string to integers all entries whose keys match a regex lipsum.*
or values are equal to 22.
MapQuery<String, Integer> q = new MapQuery<String, Integer>(); q.matching(anyOf( entry(pattern("lipsum.*"), any(Integer.class)), entry(any(String.class), eq(22)))); Map<String, Integer> results = q.execute(testList);
Querying collections of property containers
To query collections of property containers you can use the PropertyContainerCollectionQuery
class. It is parametrised with the type of values located in the queried collection. This type must be a subclass of PropertyContainer
.
The following example shows how to select all instances of MyPropertiesClass
whose property somePropertyName
value matches the regex lipsum.*
.
PropertyContainerCollectionQuery<MyPropertiesClass> q = new PropertyContainerCollectionQuery<MyPropertiesClass>(); q.matching("somePropertyName", pattern("lipsum.*")); Collection<MyPropertiesClass> results = q.execute(collection);
The next example performs the same query, but this time an own value filter was provided that does not use a costly property mechanism.
PropertyContainerCollectionQuery<MyPropertiesClass> q = new PropertyContainerCollectionQuery<MyPropertiesClass>(); q.matching(new IValueFilter<MyPropertiesClass>() { @Override public boolean matches(MyPropertiesClass obj) { return obj.somePropertyName.matches("lipsum.*"); } }); Collection<MyPropertiesClass> results = q.execute(collection);
Special case: Querying agents in aggregate
Frequently, an agent wants to query other agents located in its environment or its parent environment. Although, an aggregate is a collections of property containers it is not visible for its agents as such. In order to perform this query you need to use AgentEnvironmentQuery
. For a sample usage, take a look at the PropertiesSimpleAgent
class of the properties
example.
API description
The general form of a query is defined as:
q.from(initial selector).matching(value filter).select(value selector).process(query function).execute(target)
As one can see, four operations are currently defined:
- Initial selection: performs preselection of the values to query (e.g. first 100 values).
- Filtering by value: similar to WHERE clause in SQL.
- Selection of values (e.g. fields): similar to
SELECT x, y
clause in SQL. If not provided an original object is selected. - Functions: operations working on an entire result set, can remove, add or modify elements.
- Execution of the query.
All operations can be executed as many times as needed, their semantics for multiple execution is as follows:
- Initial selectors are executed in the order of creation (
select(first(100)).select(random(30))
will firstly select first 100 elements and then 30 random elements from these 100). - Value filters are joined with AND operator.
- Value selectors are added and all selected values will be returned as a list.
- Query functions are executed similiarly to initial selectors.
- Execution of the query performs a curently defined query over the provided target.
The GenericQuery
class does not provide a from
clause as it (usually) operates on a single object.
Constructors
Many of the query constructors requires a user to explicitly provide classes used in the target to be queried. For example:
CollectionQuery(Class<?> elementClass, Class<?> targetClass, Class<?> resultClass)
It is caused both by a Java type erasure for generics and the fact that we want to provide a user with many ways to control a query. The resultClass
in the code above is used to control a type of the result.
Standard operators
This sections presents a list of all provided query operators along with short descriptions. For more information refer to Javadocs of org.jage.query
package.
Initial selectors
first(int number)
— selects first number elements.
Value filters
lessOrEqual(T value)
,lessThan(T value)
,moreOrEqual(T value)
,moreThan(T value)
— matches respectively values: less or equal, less than, more or equal and more than the provided value. value must beComparable
.eq(T value)
— matches values equal to value.any()
— matches any value.pattern(String regex)
— matches any strings against the regex.allOf(IValueFilter... valueFilters)
— matches if all provided filters match.anyOf(IValueFilter... valueFilters)
— matches if at least one of provided filters match.
Value selectors
field(String name)
— selects a field from the object using the JavaBeans convention.
Functions
max()
,max(Comparator comparator)
— computes the maximum value according to respectively a natural or provided comparator.sum()
— computes a sum.average()
— computes an average.
How to create new query classes
There are two ways to implement a new query class:
- Inheritance from
GenricQuery
orMultiElementQuery
classes. These queries will have a behavior similar to the examples presented above. - Implementation of the
IQuery
interface. In this case only the semantics of theexecute()
method need to be left unchanged.
Both have pros and cons. If you prefer to use a semi-fluent API provided with the platform you should take a look at how CollectionQuery
is implemented, as it is the best example of a query class. If you want a highly-specialised and optimised queries it is better to implement IQuery
directly.
Query execution monitors
Some objects may want to be informed that a query is executed on them (i.e. they are an argument to the IQuery.execute()
method). The platform offers the interface IQueryAware
for this situation.
An object that implements the IQueryAware
interface will be notified at following points of query execution:
- Before execution. At this point, the object can provide a new target for the query.
- After execution (just before returning results).
Query cache
Sometimes a query is performed many times over the same object and the user wants to refresh results only at some points of time. The platform provides a simple cache based on a workplace step count to address this scenario. It works as a proxy for a query.
It is instantiated with the used query and a frequency of refreshing results (as a number of steps):
WorkplaceStepQueryResultCache<Object, Object> cache = new WorkplaceStepQueryResultCache<Object, Object>(proxiedQuery, REFRESH_AGE);
Then, it is initialized with the workplace:
cache.init(workplace);
And can be executed as a normal query:
result = cache.execute(target1);
Performance considerations
This section briefly describes best practices for working with queries, when the performance is the most important factor.
Please note that tests and results shown below are only samples of the queries behavior. They cannot be in any case treated as absolute indicators of how an operator will behave in the real application.
Although the API described above is simple and useful you need to remember that some of the operators introduce a large overhead (usually those that use the reflection). Consider these two version of the same query:
query = new CollectionQuery<QueriedObject, QueriedObject>(QueriedObject.class); query.matching("intValue", eq(10));
query = new CollectionQuery<QueriedObject, QueriedObject>(QueriedObject.class); query.matching(new IValueFilter<QueriedObject>() { @Override public boolean matches(org.jage.query.PerformanceTest.QueriedObject object) { return object.getIntValue() == 10; } });
A very simple test can be performed by executing these queries many times on a collection:
for (int i = 0; i < 1000000; i++) { Collection<QueriedObject> result = query.execute(target); }
Sample results are as follows:
- 30.790 s
- 1.056 s
As you can see, a built-in field value matcher (that uses the reflection) may perform up to 30 times worse than a customised value matcher that simply calls a method.
It is worth to mention that the no-operation query (so called "empty query") introduces some overhead itself. If high performance is required and provided operators can be easily replaced by a custom code it may be better to create a clean implementation of the IQuery
interface.
List of operators that can have large performance penalty
These operators and operations use reflection or other operations that introduce a large performance penalty:
field()
fieldValue()
propertyValue()
GenericQuery.select(String... fields)
,MultiElementQuery.select(String... fields)
and the same method in subclasses.GenericQuery.matching(String fieldName, IValueFilter<S> filter)
,MultiElementQuery.matching(String fieldName, IValueFilter<S> filter)
and the same method in subclasses.