This thin library adds a small xtend DSL for querying the Onyx Database.
It has the following features:
- simple query syntax with boolean operators
- type-safe static compile time query checks
- since it is static, in an IDE you get code completion for fields
- and since it is static, if your model changes, your queries break and can be corrected
- support for joins across your entities
- observe query changes
- xtend-onyx
- Table of Contents
- Example
- Getting Started
- Querying entities
- Getting results
- Updating entities
- Deleting entities
- Listening for changes
import static extension nl.kii.onyx.OnyxExtensions.*
val db = factory.persistenceManager
val person = new Person => [
firstName = 'Foo'
lastName = 'Bar'
address = new Address => [
street = 'FooStreet'
houseNr = 99
]
]
db.saveEntity(person)
val results = db
.query (Person.Data)
.where [ firstName != null && address_houseNr == 99 ]
.order [ -id ]
.list
println(results)
This project uses Gradle. In the project root, type:
- gradle build - To build the project and run a test.
- gradle eclipse - To generate the Eclipse projects to import.
- gradle idea - To generate the IntelliJ IDEA projects to import.
- gradle install - To install the library on your local maven repository.
Xtend-onyx has only dependencies on the standard xtend java library and the onyx database.
It is not yet available on Maven Central, so to use it you have to install it to your local repository.
To import it after you have gradle installed it locally (see above), add this dependency to your own project:
compile "net.sagittarian.xtend-onyx-core:1.3"
and add the Onyx database dependency as well, eg:
compile "com.onyxdevtools:onyx-database:$onyxVersion"
To use xtend-onyx, you must annotate your entities with the @OnyxFields and OnyxJoins Active Annotations. This will make the metadata from your entity statically available to the xtend-onyx library. Every entity you annotate will get a Data subclass with metadata selectors you can use when querying.
For example, say you have a class Person:
@Entity
class Person extends IManagedEntity {
@Attribute String firstName
}
You will have to add the xtend-onyx annotations to it like this:
@OnyxFields
@OnyxJoins
@Entity
class Person extends IManagedEntity {
@Attribute String firstName
}
This will create the metadata class Person.Data for you. This class will provide you selectors. Person.Data will have this method added:
def Field<String> firstName()
These selectors you can then use for querying your entities.
These methods can be used in a TypedQuery. This Xtend wrapper builds an Onyx Query. This is used in combination with the OnyxExtensions static extensions. This extension class provides the query(PersistenceManager, MetaData) method, that creates a TypedQuery instance to work with.
For example:
import static extension net.sagittarian.onyx.OnyxExtensions.*
val typedQuery = db.query(Person.Data)
Note: you need to pass the metadata class, not the type you are querying. In this case, that means you need to pass Person.Data, not Person. This is the metadata class that was generated for you by the active annotations you added to your entity.
This query can be given commands to filter entities, such as:
.select
.where
.order
After setting these properties as well as others, you can call .build too generate the Onyx Query. However, unless you want to re-use an Onyx Query object, you will normally directly call one of the following:
.list
.lazyList
.first
.update
.delete
.listen
Below is an explanation of how to use each of the operations the typed query provides.
Lets you filter the stored entities with query criteria.
query.where( (Metadata<T>)=>QueryCriteria )
The metadata instance provides you with the field and join selectors, and the extension provides you with handy overloads that let you create criteria with these selectors. For example:
selector == value
This translates to new QueryCritera(selector.name, EQUALS_TO, value). Similar overloads exist for the following operators:
! not
== equals
!= not equals
> greater than
>= greater than or equals
< smaller than
<= smaller than or equals
&& and
|| or
Example:
db
.query (Address.Data)
.where [ street == 'Busystreet' && houseNr >= 20 ]
You can use full combined expressions, including braces and negation:
db
.query (Address.Data)
.where [
!(street == 'Busystreet' || houseNr >= 20)
&& street != null
]
The correct operator precedence is automatically enforced (this is a feature of the Xtend operators).
If you use where multiple times on a query, it will perform a logical AND between all of your criteria. This has the same result as the query above:
db
.query (Address.Data)
.where [ street == 'Busystreet' ]
.where [ houseNr >= 20 ]
The @OnyxJoin annotation lets you use join selectors in queries. For example, given an Address class with a relationship occupants of type Person, and each Person entity having a firstName, you can do this:
db
.query (Address.Data)
.where [ occuptants_firstName == 'Jason' ]
You can check if an attribute is one of a list of attributes using selector.in() :
// directly specify
query.where [ name.in('Josh', 'Mary', 'John') ]
// or as a list:
val wantedPeople = #['Josh', 'Mary', 'John']
query.where [ name.in(wantedPeople) ]
// everyone but those people:
query.where [ name.notIn(wantedPeople) ]
Using selector.in() can work particulary well with enum types.
There are also these additional string field operations:
// positive expressions
selector.startsWith(text) // field value must start with the text
selector.contains(text) // field value must contain this text
selector.matches(regexp) // field value matches the regular expression
// negative versions of the above
selector.notStartsWith(text)text
selector.notContains(text)
selector.notMatches(regexp)
Lets you change the order of the returned entities.
.order( (Metadata<T>)=>QueryOrder … )
The metadata instance provides you with the field and join selectors, and the extension provides you with handy overloads that let you create QueryOrder instances for these selectors. For example:
+selector // order by the selector in increasing order
-selector // order by the selector in decreasing order
You can call order multiple times, or pass multiple closures.
Examples:
db
.query (Address.Data)
.where [ street == 'Busystreet' && houseNr >= 20 ]
.order [ +street ]
To sort first by street ascending, and then houseNr descending:
db
.query (Address.Data)
.where [ street == 'Busystreet' && houseNr >= 20 ]
.order [ +street ]
.order [ -houseNr ]
This has the same result:
db
.query (Address.Data)
.where [ street == 'Busystreet' && houseNr >= 20 ]
.order ( [ +street ], [ -houseNr ] )
Get the results of the query as a List.
.list
Example:
val results = db
.query (Address.Data)
.where [ street == 'Busystreet' && houseNr >= 20 ]
.order [ +street ]
.list
println(results) // prints addresses
This performs the same basic function as List, but only gets the matching results and does not yet hydrate them into full entities. The moment you get something from the list, the entity is actually fetched. This can help query large resultsets.
Unlike the Onyx resulted list, this list can be iterated over normally:
val results = db
.query (Address.Data)
.lazyList
for(result : results) {
println(result) // gets and hydrates the result entity
}
To tune performance, you can tell Onyx to only fetch the fields you are interested in for each fetched entity.
query.list( (MetaData)=>Field<?>… fieldFns )
For example, to only fetch the firstName for each entity:
val List<String> results = db
.query (User.Data)
.list [ firstName ]
You can also select multiple fields, in which case you will get a list of map<field, value>.
val List<Map<String, ?> results = db
.query (User.Data)
.list( [ firstName ], [ lastName ] )
These selections also work with lazyList:
val List<Map<String, ?> results = db
.query (User.Data)
.lazyList( [ firstName ], [ lastName ] )
You can tell Onyx that you only want distinct results, meaning that the combination of values for the fields you selected is unique.
val List<Map<String, ?> results = db
.query (User.Data)
.distinct
.list( [ firstName ], [ lastName ] )
If in the above example you had two users named ‘Ben Johnson’, only the first match would be returned.
To simply get the first result only, perform .first:
println(db.query(Address.Data).first)
Get a count of the amount of entities matching your query with .count:
// how many addresses do we have?
println(db.query(Address.Data).count)
There are some properties you can set on the query to set how to page through results:
- .skip(amount) : skip [amount] results.
- .limit(amount) : at maximum return [amount] results.
- .page(pageNr) : get a single page of results, given an amount of results per page you set with limit. This can also be combined with skip, the paging will then start after the skipped entities.
- .range(first..last) : return only the [first]th to at most the [last]th result. Also combines with skip and limit, in that it will start counting after the skipped entities, and return at most limit entities.
You update entities by calling .set [ ] to tell the query what values to set, and then .update to perform the changes.
Update an entity attribute. Use the => operator to assign a value inside the closure.
db
.query (User.Data)
.set [ username => 'bobby' ]
.update // changes all usernames to bobby
You can also perform multiple set commands:
db
.query (User.Data)
.set [ username => 'bobby' ]
.set [ hobby => 'knitting' ]
.update
Or combine them in a single set:
db
.query (User.Data)
.set ( [ username => 'bobby' ], [ hobby => 'knitting' ] )
.update
Combine setting with .where [ ] to only change some entities:
db
.query (User.Data)
.set [ username => 'bobby' ]
.where [ username == 'robby' ]
.update // changes robby user to bobby
The update call returns the amount of entities that were updated.
End a query with .delete to remove all entities that match the query.
db
.query (User.Data)
.delete // deletes all users
db
.query (User.Data)
.where [ username == 'robby' ]
.delete // deletes robby
The delete call returns the amount of entities that were removed.
You can observe changes as they occur on your database with the .listen() method. You pass a QueryListener implementation to listen for any changes that match a query you specify. For example:
val listener = db
.query (User.Data)
.where [ firstName == 'Jones' ]
.listen (new QueryListener<User> {
onItemAdded(User jones) {
}
onItemRemoved(User jones) {
}
onItemUpdated(User jones) {
println('Jones was updated!')
}
})
// later:
listener.stopListening
Now when a user with firstName Jones was updated, the onItemUpdated method will be called with the user.
When you create a listener, you usually want to keep it open for a while to listen for database updates. The listener is attached to the open session. Therefore in order to not leave listeners dangling in memory, you always need to stop them.
When you call .listen(), you will get back a Listener. You stop listening by calling Listener.stopListening().