-
Notifications
You must be signed in to change notification settings - Fork 8
Collectors
As described SQLstore parses queries specified in an application into an AST. This AST is not only used to create SQL queries, but also to prepare the processing of query statement results by creating so-called Collectors.
A collector is basically a function that receives a single JDBC result set row and returns a JavaScript object with the column names as property names and the JDBC value translated into its JavaScript equivalent as values.
SQLstore has three different types of collectors, which are created based on the select
portion of the query:
The simplest type of collector is the ValueCollector
. Its job is basically to retrieve column values in a JDBC result set row, convert them and return an object containing these values. So the query
select birthdate from Author where name = 'Mark Twain'
would be translated into the following SQL query:
SELECT `Author`.`birthdate` FROM `Author` WHERE `Author`.`name` = ?
(the placeholder ?
is representing the string "Mark Twain"). Based on the AST of the above query SQLstore knows that every resultset row will contain a single value, and that it should just collect them. So it creates an instance of ValueCollector
.
When SQLstore receives the JDBC result set for the executed query, it loops over all its rows, calling the collect()
method of the ValueCollector for each of them. This method retrieves the values from the resultset rows, converts them into their JavaScript equivalent and returns them.
In this example the result of the query is an array containing a single Date object with the birthdate of the author (note that all queries in SQLstore return an array, even if the query resulted in a single value, as in this example).
If the select
portion of the query contains more than one property, the resulting array would contain one or more objects, each of them containing the received values stored in properties named after the relevant part of the select clause, eg.:
{
"name": "Mark Twain",
"birthdate": new Date(1835, 10, 30, 0, 0, 0, 0)
}
If the query uses column resultset names (eg. select date_birth as birthDate …
), the specified alias is used as property name.
EntityCollectors are used whenever an application needs to retrieve an entity from the database. Entities in SQLstore are loaded lazily, so what's actually retrieved from the database is just the ID of the entity, nothing more. Imagine the query
from Author where id = 1
This query is translated into an SQL query looking something like this:
SELECT `Author`.`id` FROM `Author` WHERE `Author`.`id` = ?
(the value 1
is used as JDBC prepared statement value for the placeholder ?
). Using the AST of this query SQLstore determines that the query result rows it will receive from the database will contain just a single value, the ID of the Author
entity to construct. So it creates an instance of EntityCollector
, loops over the received JDBC result set and calls the collector's collect()
method for each of them. Inside the collect()
method the following happens:
- If the Entity Cache is enabled, it checks if it contains an entry for the key
Author#1
(the cache key consists of the entity constructor's name and the ID, separated by a hash). If the cache has an entry for this key, the value is retrieved and SQLstore skips to step 3 below. - The ID value in the resultset row is retrieved and converted into a JavaScript number. Using this an entity key is constructed (these keys are the primary identifiers of entities within SQLstore).
- An instance of the
Author
entity is created, passing at least the key as argument. If the property values of this Author instance have already been retrieved and cached before, the newly created instance is right away populated with the data, otherwisenull
as data argument is passed to the entity constructor. Finally thecollect
method returns the createdAuthor
instance.
The result returned to the application issueing the query is an array containing these entity instances.
Imagine a query
select * from Author where id = 1
First SQLstore translates this query into something like the following:
SELECT `Author`.`id`, `Author`.`name`, `Author`.`birthdate` FROM `Author` WHERE `Author`.`id` = ?
As you see, Author.*
has been expanded into all columns defined in the Author mapping. Based on this expanded select
clause of the query's AST SQLstore knows that the resultset rows it will receive from the database will contain all columns defined in the Author
table.
Similar to the EntityCollector
this type of collector has a collect()
method, which does the following:
-
If the entity cache is enabled, it checks if it contains an entry for the key
Author#1
. If the cache has an entry for this key, the value is retrieved from the cache and SQLstore skips to step 3 below. -
SQLstore loops over all columns defined in the
Author
entity mapping and retrieves the values from the resulset row received from the database. These values are stored in an object that roughly looks like this:{ "id": 1, "name": "Mark Twain", "birthdate": new Date(1835, 10, 30, 0, 0, 0, 0) }
In addition an entity key is created (it contains the constructor name
Author
and the ID of the record). Both the key and the data object are put into the entity cache (if enabled, of course). -
As with the
EntityCollector
above, thecollect()
method creates a newAuthor
instance, passing it the key and the data object, and returns it.
The result of this collector is the same as the one of the EntityCollector
above: an array containing Author
instances, but this time they already contain the complete data stored in the database, so SQLstore doesn't need to access the database again just to retrieve an author's data.
It's important to keep in mind that SQLstore doesn't cache created entity instances (ie. instances of the Author
constructor) in its entity cache. The reason for this is that RingoJS applications are mostly multi-threaded, and the entity cache is shared among all application threads.
Imagine two threads accessing the same Author
instance. If the cache would contain the final entity instances, both threads would operate on the same instance. Now if one of them would modify the author, the change would immediately be visible to the other thread, thus changing its context. To prevent that every thread in an application receives its own instance of an entity, each of them wrapping a shared object containing the data received from the database for it.
What happes if one thread modifies its Author
instance and commits it into the datase is that the data object for this instance (containing the new data) is put into the entity cache, replacing the former cached data object. This however doesn't affect the other running thread, since its Author
instance still wraps the former version of the data object. This effectively separates the threads of an application and avoids all sorts of concurrency problems.
To sum it up every entity instance consists of two objects: a short living entity wrapping a long living data object.