This document proposes an implementation of LINQ-style querying for ActionScript 3 collections. It provides examples of filtering, sorting, grouping and selecting data from collections using query syntax. The implementation would include interfaces like IQuery and ICondition to define query and filter objects. CollectionQuery would be the main class, allowing queries to be built and executed on a target collection. Predicates would represent individual filter conditions, and Conditions groups of predicates. The proposal outlines methods, properties and classes required to support queries on nested and complex objects.
2. Introduction
Draft implementation, just an idea, would be great if
we have something like this in ActionScript (but
more robust, complex, optimized of course)
Purpose – collection sorting, filtering, grouping
wrapper
OO-Style LINQ to Objects lib for flex
3. Usage examples – first, last, any, itemAt
//get first with title containing case insensitive string “Voislav Seselj”
var query:IQuery = new CollectionQuery(_news);
var firstItem:NewsFeed = query.where(new
Conditions().contains("title", “Voislav Seselj", false)).first() as NewsFeed;
//last item published in 2013 or 2014
var lastItem:NewsFeed = query.where(new
Conditions().eq("year", 2014).OR.eq("year", 2013)).last() as NewsFeed;
var usersQuery:IQuery = new CollectionQuery();
if(usersQuery.from(users).where(new Conditions().eq("name", "Peter Pan")).any())
{
//excellent, there is a user (or users) with name "Peter Pan“
}
//get third adault from the end of the collection ordered by name ascending and lastname
//descending.
var user:User = new CollectionQuery(users)
.where(new Conditions().gte("years", 18))
.orderBy("name").orderBy("lastname", false)
.itemAt(2) as User;
4. Usage examples – where, select, limit
//get users for given conditions ordered by company and then by years property
var users:IList = usersQuery.where(new Conditions().lt("years", 18).gt("years", 5).OR
.eq("company", "Company 1"))
.orderBy("company“).orderBy("years")
.execute() as IList;
//select collection containing anonymous objects with properties given in select clause
//extracted from user objects, for given conditions, ordered by salary with ascending order
//and compare as numerics.
//Limit to maximum 10 results
var users:IList = new CollectionQuery()
.select("name, lastname, company, salary")
.from(users)
.where(new Conditions().gte("salary", 40000).lte("salary", 45000)) //AND by default
.orderBy("salary", true, true)
.limit(10).execute() as IList;
5. Usage examples – nested complex objects
//select streets from complex member “address” for each user in users collection
var streets:IList = usersQuery.select("address.street").from(users).execute() as IList;
//select anonymous objects containing user’s name, lastname, address->number, address//>street, address->uniqueId->value, for users having address number greater then 400 in
//Kragujevac city, or having address number lower then 400 and live in Belgrade city.
//Order list by city ascending and by address number descending comparing address.number
//property as numbers.
var users:IList = usersQuery
.select("name, lastname, address.number, address.street, address.uniqueId.value")
.from(users)
.where(new Conditions()
.gt("address.number", 400).eq("address.city", "Kragujevac").OR.
lt("address.number", 400).eq("address.city", "Belgrade"))
.orderBy("address.city")
.orderBy("address.number", false, true)
.execute() as IList;
6. Usage examples – groupBy
//group users named Marija or Slavisa by city, ordere by name and then by lastname
var query:IQuery = new CollectionQuery();
var groupResults:IQueryGroupResult = query
.select("id, name, lastname, address.street") //get anonimous objects
.from(users)
.where(new Conditions()
.eq("name", "Slaviša").OR
.eq("name“,"Marija").AND.contains("lastname", "ić"))
.orderBy("name").orderBy("lastname", false) //order by name , lastname descending
.groupBy("address.city")
//group by city
.execute() as IQueryGroupResult;
//get grouped results
for each(var city:Object in groupResults.keys) //iterate through key collection
{
printUsersFromCity(key, groupResults[key] as IList); //and print collection for each key
}
if(groupResults.keys.contains("Kragujevac")) //test if there is a key
{
var kragujevcani:IList = groupResults["Kragujevac"] as IList; //print the collection
}
8. IQuery
/**
* Query interface
* @author Slavisa
*/
public interface IQuery
{
function select(value:String = null):IQuery;
//select columns
function from(coll:IList):IQuery;
//sets collection to be queried
function where(conditions:ICondition):IQuery;
//sets condition chain
function groupBy(property:String):IQuery;
//sets group keys
function orderBy(property:String, ascending:Boolean = true, numeric:Boolean = false):IQuery; //order
by given property
function limit(count:int = 100):IQuery;
//limits result’s item count
function first():Object;
//returns first or null
function last():Object;
//returns last or null
function itemAt(index:int):Object;
//returns item by the given index, or null
function execute():Object;
//actual execution, returns IList or IQueryGroupResult object
function any():Boolean;
//determines if at least one result exists
}
9. ICondition
/**
* Condition interface
* @author Slavisa
*/
public interface ICondition
{
function eq(prop:String, value:Object):ICondition;
//equal
function diff(prop:String, value:Object):ICondition;
//not equal
function contains(prop:String, value:String, caseSensitive:Boolean = true):ICondition; //contains
function lt(prop:String, value:Object):ICondition;
//lower than
function lte(prop:String, value:Object):ICondition;
//lower than or equal
function gt(prop:String, value:Object):ICondition;
//greater than
function gte(prop:String, value:Object):ICondition;
//greater than or equal
function get OR():ICondition;
function get AND():ICondition;
function get root():ICondition;
}
//OR – new condition group
//AND – new condition group
//get root condition
10. IQueryItemResult, QueryResultItem, IQueryGroupResult
/**
* Query Item result interface
* @author Slavisa
*/
public interface IQueryItemResult
{
}
/**
* Query Item result dynamic class
* Stores custom selection (anonimous object)
* @author Slavisa
*/
public dynamic class QueryResultItem
implements IQueryItemResult
{
}
/**
* Query Grouped results
* @author pokimsla
*/
public interface IQueryGroupResult
{
function get keys():ArrayCollection;
function get length():int;
}
//gets keys collection
//gets number of groups
11. QueryGroupResult
public dynamic class QueryGroupResult extends Dictionary implements IQueryGroupResult
{
//Gets array collection of group keys
public function get keys():ArrayCollection
{
var keys:ArrayCollection = new ArrayCollection();
for(var key:Object in this)
{
keys.addItem(key);
}
return keys;
}
//number of keys
public function get length():int
{
return keys.length;
}
}
12. Predicate
/**
* Predicate class
* @author Slavisa
* Used for combining multiple conditions joined with AND operator
*/
[Bindable]
public class Predicate implements ICondition
{
/* Predicate types */
public static const EQ:String = "equal";
public static const DIFF:String = "different";
public static const CONTAINS:String = "contains";
public static const LT:String = "less";
public static const GT:String = "greater";
public static const GTE:String = "greater or equal";
public static const LTE:String = "lower or equal";
/* operators */
public static const OPERATOR_OR:String = "or";
public static const OPERATOR_AND:String = "and";
public var property:String;
//property for comparison
public var value:Object;
//value to compare with
public var type:String;
//predicate type
public var conditions:Conditions; //joining conditions (group of predicates to which this predicate belongs)
public var attributes:Object;
//additional attributes
private var _root:ICondition;
//root condition
13. Predicate
public function Predicate(cond:ICondition, prop:String, val:Object, type:String, attributes:Object = null)
{
this._root = cond.root; //set the root condition
this.property = prop;
this.value = val;
this.type = type;
this.conditions = cond as Conditions;
this.conditions.addPredicate(this); //authomaticly add me to the predicate list
this.attributes = attributes;
}
//creates next predicate (not equal)
public function diff(prop:String, value:Object):ICondition
{
return createNextPredicate(prop, value, DIFF);
}
//creates next predicate (equal)
public function eq(prop:String, value:Object):ICondition
{
return createNextPredicate(prop, value, EQ);
}
…
//create next predicate for given values
private function createNextPredicate(prop:String, value:Object, type:String, attributes:Object = null):ICondition
{
var pred:Predicate = new Predicate(conditions, prop, value, type, attributes);
return pred;
}
14. Predicate
// Creates new condition with OR relation
public function get OR():ICondition
{
var condition:Conditions = new Conditions();
condition.type = OPERATOR_OR;
condition.root = root;
this.conditions.next = condition;
return condition;
}
//creates new condition with AND relation
public function get AND():ICondition
{
var condition:Conditions = new Conditions();
condition.type = OPERATOR_AND;
condition.root = root;
this.conditions.next = condition;
return condition;
}
//gets root condition
public function get root():ICondition
{
return _root;
}
}
15. Conditions
/**
* Condition contains group of predicates, used for joining multiple condition groups (predicates)
* @author Slavisa
* <br/>
* Condition chain boolean result is calculated from last to root condition group
*/
public class Conditions implements ICondition
{
private var _predicates:ArrayCollection=new ArrayCollection();
//predicate collection
private var _next:ICondition;
//next condition in a row
private var _root:Conditions;
//starting condition
public var type:String;
//starting condition shouldn't have a type property populated. EQ, OR, AND, LT...
//Condition contains group of predicates, used for joining multiple condition groups (predicates)
public function Conditions()
{
_root = this;
}
/**
* add predicates to a collection
* @param predicate
* predicates are validated in groups with ANR logical operator
*/
public function addPredicate(predicate:ICondition):void
{
_predicates.addItem(predicate);
}
16. Conditions
//creates and appends diff predicate
public function diff(prop:String, value:Object):ICondition
{
return new Predicate(this, prop, value, Predicate.DIFF);
}
//creates and appends eq predicate
public function eq(prop:String, value:Object):ICondition
{
return new Predicate(this, prop, value, Predicate.EQ);
}
//… other predicates, getters and setters
//OR condition does nothing on Conditions instance
public function get OR():ICondition
{
return this;
}
//AND condition does nothing on Conditions instance
public function get AND():ICondition
{
return this;
}
}
17. CollectionQuery
/**
* Actual collection query implementation
* @author Slavisa
*/
public class CollectionQuery implements IQuery
{
private var _coll:IList; //collection
private var _conditions:ICondition; //conditions
private var _comparatorGroups:ArrayCollection; //comparator groups built before collection iteration
private const _comparatorMap:Object = createComparatorMap(); //comparator delegates
private var _selectColumns:String = null; //select columns. If null, row items are selected
private var _limitCount:int = 0; //limited item count
private var _groupBy:String = null; //group by this property
private var _orderBy:ArrayCollection = new ArrayCollection(); //order by these properties
private var _hasGroupBy:Boolean = false;
private var _hasOrderBy:Boolean = false;
//ctor
public function CollectionQuery(coll:IList = null, selectColumns:String = null)
{
_coll = coll;
this._selectColumns = selectColumns;
}
19. CollectionQuery
//sets select properties
public function select(value:String = null):IQuery
{
_selectColumns = value;
return this;
}
//sets target collection
public function from(coll:IList):IQuery
{
_coll = coll;
return this;
}
//sets conditions root
public function where(conditions:ICondition):IQuery
{
this._conditions = conditions.root;
return this;
}
/*
Similar for :
groupBy
orderBy
limit
*/
20. CollectionQuery
public function first():Object
{
if(_hasGroupBy)
throw new Error("First() is allowed only for non-grouped results");
var result:IList = runExecution(1) as IList;
return result.length > 0 ? result[0] : null;
}
public function last():Object
{
if(_hasGroupBy)
throw new Error("Last() is allowed only for non-grouped results");
var result:IList = runExecution(1, true) as IList;
return result.length > 0 ? result[0] : null;
}
public function itemAt(index:int):Object
{
if(_hasGroupBy)
throw new Error("ItemAt() is allowed only for non-grouped results");
if(index < 0)
throw new ArgumentError("Invalid argument for itemAt method");
var result:IList = runExecution(index + 1) as IList;
return result.length > (index + 1) ? null : result[index];
}
21. CollectionQuery
public function execute():Object //interface impl
{
return runExecution(_limitCount);
}
//actual query execution
private function runExecution(limit:int = 0, reverse:Boolean = false):Object
{
implementation details excluded for insufficient space, if anyone cares send me an email to slavisapokimica@yahoo.com, I’ll
be happy to provide all you need
}
private function orderCollection(collection:ArrayCollection):void
{
if(_orderBy.length == 0)
return;
var sort:Sort = new Sort();
sort.fields = new Array();
for each(var fieldDef:Object in _orderBy)
{
var sortField:SortField = new SortField(fieldDef.field, false, !fieldDef.ascending, fieldDef.numeric);
sort.fields.push(sortField);
}
collection.sort = sort;
collection.refresh();
}
22. CollectionQuery
/**
* Creates result item
* @param item
* @return Row Item or anonymous object with properties generated from selectColumns variable
*/
private function createResultItem(item:Object):Object
{
if(_selectColumns == null) //return row item
return item;
var columns:Array = _selectColumns.split(",");
//split columns
if(columns.length == 1)
return extractPropertyValue(item, columns[0]);
//if only one property, than return it
var res:QueryResultItem = new QueryResultItem();
//anonymous dynamic object
for each(var column:String in columns)
//populate anonymous object
{
var col:String = StringUtil.trim(column);
var newPropname:String = col.replace(/[.]/g, '_');
res[newPropname] = extractPropertyValue(item, col); //set property value
}
return res;
}
23. CollectionQuery
/**
* extracts property from given item
*/
private function extractPropertyValue(item:Object, property:String):Object
{
var single:String = StringUtil.trim(property);
if(single.indexOf(".") == -1)
{
if( !item.hasOwnProperty(single) )
throw new ArgumentError("Property with name "" + single + "" does not exist!");
return item[single];
}
//recursive call when extracting complex member’s properties
return
extractPropertyValue(item[property.substring(0, property.indexOf("."))], property.substring(property.indexOf(".") + 1));
}
24. CollectionQuery
/**
* Actual filtering logic
* @param item collection item currently being validated
* @param validationGroups condition groups to be validated with
* @return Boolean
* <ul>
* <li>validation groups empty - return true</li>
* <li>single item in validation group - return predicate validation result</li>
* <li>iterate through all condition groups and its predicates and validate them all</li>
* </ul>
*/
private function filterFunction(item:Object, validationGroups:ArrayCollection):Boolean
{
if(validationGroups.length == 0)
return true;
if(validationGroups.length == 1)
return validatePredicates(item, _comparatorGroups[0].predicates);
var result:Boolean = validatePredicates(item, _comparatorGroups[0].predicates);
for (var i:int = 0; i < _comparatorGroups.length - 1; i++)
{
result = _comparatorGroups[i].operation == Predicate.OPERATOR_AND ?
result && validatePredicates(item, _comparatorGroups[i + 1].predicates) :
result || validatePredicates(item, _comparatorGroups[i + 1].predicates);
}
return result;
}
25. What’s next
Optimization
Validation
Interface expansion
“In” and “Between” conditions
Joins
Comparison between two properties
Query reset
Nested conditions
And much more…
26. Also kewl to have - code snippets I
<template autoinsert="true"
context="com.adobe.flexide.as.core.codetemplates.action_script"
deleted="false" description="Query first item by the given conditions"
enabled="true" name="query_first">
var query:IQuery = new CollectionQuery(${_collection});
var firstItem:${type} = query.where(new
Conditions().${operation:values(eq, lt, lte, gt, gte, diff,
contains)}("${prop}", ${value})).first() as ${type};
</template>
<template autoinsert="true"
context="com.adobe.flexide.as.core.codetemplates.action_script"
deleted="false" description="Order queried collection"
enabled="true" name="query_order">
var query:IQuery = new CollectionQuery(${_collection});
var orderedResults:IList = query.where(new Conditions()
.${operation:values(eq, lt, lte, gt, gte, diff, contains)}("${prop}", ${value}))
.orderBy("${orderProp}", ${ascending:values(true, false)}, ${numeric:values(false, true)})
.execute() as IList;
</template>
27. Code snippets II
<template autoinsert="true"
context="com.adobe.flexide.as.core.codetemplates.action_script"
deleted="false" description="Grouping query results"
enabled="true" name="query_group">
var query:IQuery = new
CollectionQuery(${_collection});
var groups:IQueryGroupResult = query
.where(new Conditions()
.${operation:values(eq, lt, lte, gt, gte, diff, contains)}("${prop}", ${value}))
.groupBy("${groupProperty}")
.execute() as IQueryGroupResult;
for each(var key:Object in groups.keys)
{
var groupedItems:ArrayCollection = groups[key] as ArrayCollection;
}
</template>