What is new
in
Gaelyk 2.0
Vladimír Oraný
What's new in Gaelyk 2.0
- Gaelyk in the nutshell
- (Surprise hot from the oven)
- Breaking changes
- Enhancements
Gaelyk in the nutshell
Lightweight toolkit for a Google App Engine
More information
Biggest application running Gaelyk
Serving 1.2M request per day
Glide
- Just 5 days from the first public release
-
Command line utility created by Kunal Dabir (@kdabir)
- Built on the top of Gaelyk
- Removes all the GAE/Gradle clutter
-
https://github.com/kdabir/glide
- Still work in progress
Breaking Changes
-
Groovy 2.0 support
-
Adding tasks asynchronously by default
-
Removed automatic @Version property
-
Single asterisk matching in routes
- Minor API changes
Groovy 2.0 Support
- farewell
GaelykCategory
- short cuts available wherever you want
-
@CompileStatic
wherever possible
// use(GaelykCategory){ // no longer compile
MyPogo mypogo = new MyPogo(a: 'a', b: 'b', c: 'c')
Entity entity = mypogo as Entity
entity.save()
// }
Script Extension Modules
Helper methods and tag-like functions
class ScriptExtension {
void ifAuthenticated(Script self, Closure body){
self.with{
if(user){
body(user)
}
}
}
}
// someTemplate.gtpl
<% ifAuthenticated { user -> %>
Hello $user.nickname!
<% } %>
Gradle FatJar Plugin
Deploying Gaelyk 2.0 needs Gradle FatJar 0.2+
dependencies {
classpath 'org.gradle.api.plugins:gradle-gaelyk-plugin:0.4.1'
classpath 'org.gradle.api.plugins:gradle-gae-plugin:0.8', {
exclude module: "gradle-fatjar-plugin"
}
classpath 'eu.appsatori:gradle-fatjar-plugin:0.2'
classpath 'org.gradle.api.plugins:gradle-gae-geb-plugin:0.3'
}
Otherwise the extension modules
descriptors are not merged
Adding Tasks Asynchronously
Before 2.0 the call was waiting for response...
TaskHandle handle = defaultQueue << [url: '/task.groovy']
... even it was barely used
defaultQueue << [url: '/task.groovy']
Since 2.0 the call returns future in the case anybody cares
Future<TaskHandle> future = defaultQueue << [url: '/task.groovy']
Removed automatic @Version property
Version is automatic column for optimistic locking
It is pretty expensive to fetch (~1s)
@Entity class Book {
// magic long id property is still added
// @Key Long id
// no more version property added automatically
// @Version Long version
@Indexed String title
}
Book book = new Book(title: 'It')
assert book.hasProperty('id')
assert !book.hasProperty('version')
More strict asterisk matching
No more greedy routes
get '/agent/*/@id', forward: '/agent.groovy?id=@id'
'/agent/bond/7' => '/agent.groovy?id=7'
//before 2.0 the '*' in routes was greedy
'/entry/james/bond/7' => '/agent.groovy?id=7'
//since 2.0 the '*' matches just to the closest slash
'/entry/james/bond/7' => '/agent.groovy?id=james/7'
// use '**' to get the old behaviour back
get '/agent/**/@id', forward: '/agent.groovy?id=@id'
Minor API Changes
LifecycleManger shutdown hook
// use method call with closure
lifecycle.shutdown{...}
// instead of assigning closure as variable
// lifecycle.shutdown = {...}
Parsing XMPP message XML payload
// use method call xml()
message.xml()
// instead of property access
// message.xml
Enhancements
- Search
- Datastore
- Routes
- Plugins
- Parameter handling
Search
More options for indexes
Search DSL
Recoverable asynchronous search
Be sure you're running HRD!
Most of the examples only works when deployed
More options for indexes
search.index('restaurants').put { document(id: 'pakwaan') { title text: 'Pakwaan Indian Restaurant'
rating number: 4 opensAt number: 10 closesAt number: 24
averagePrice number: 7 menu text: ''' Chicken Tikka 6 pcs - Marinated boneless chicken are with yoghurt, spices and cooked in tandoor.
Chicken Seekh Kebab - Minced chicken with ginger flavour, lemon, and butter with green coriander
'''
// new tags atom: ['lunch menu', 'indian', 'take away'] location geoPoint: [50.098009,14.403806] }
}
Search on GAE
QueryOptions.Builder queryOptions = QueryOptions.newBuilder()
queryOptions.idsOnly = true
queryOptions.numberFoundAccuracy = 1000
SortOptions.Builder sortOptions = SortOptions.newBuilder()
sortOptions.limit = 1000
SortExpression.Builder exp = SortExpression.newBuilder() exp.expression = 'distance(location,geopoint(50.07543,14.436936))'
exp.defaultValueNumeric = 0
exp.direction = SortDirection.DESCENDING
sortOptions.addSortExpression(exp)
queryOptions.sortOptions = sortOptions
Query.Builder queryBuilder = Query.newBuilder()
queryBuilder.options = queryOptions
Query query = queryBuilder.build('tags: "take away")
Search on GAE
SearchService search = SearchServiceFactory.searchService
Index index = search.getIndex(
IndexSpec.newBuilder().setName(indexName).build()
)
Results<ScoredDocument> results = index.search(query)
def ids = results.results*.id
Search DSL
def results = search.search {
select ids from restaurants where tags =~ 'take away' sort asc by distance(location, geopoint(50.07543,14.436936)), 0
number found accuracy 1000
limit sort to 1000 }
def now = 22.5 // 22.30
def results = search.search {
select title, opensAt, closesAt
from restaurants
where opensAt <= now
and closesAt >= now
sort desc by rating, 0
}
results.results*.title
Search DSL
def searchText = 'chicken' def results = search.search { select snippet: snippet(query, menu),
distance: distance(location, geopoint(50.07543,14.436936)) from restaurants where menu =~ ~searchText sort asc by distance(location,
geopoint(50.07543,14.436936)), Double.MAX_VALUE } for(result in results.results){ println "$result.title: $result.snippet ($result.distance)" }
Recoverable Async Search
def results = search.searchAsync(3) { select ids from restaurants where tags =~ 'take away' sort asc by distance(location, geopoint(50.07543,14.436936)), 0
number found accuracy 1000
limit sort to 1000 }
Future results = 3 * {
search.searchAsync { select ids from restaurants where tags =~ 'take away' } }
Future key = 3 * { entity.asyncSave() }
Datastore
Query result lists and iterables
Restarting queries automatically
Saving QueryBuilder for later use
Parent support for @Entity
Better coercion performance
Cross-group transactions
Minor updates
Query result lists and iterables
Datastore operations now returns QueryResultList and QueryResultIterable where possible
QueryResultList<Entity> restaurants = datastore.execute {
from Restaurant
if(params.cursor){ startAt params.cursor }
limit Math.min((params.limit ?: 10) as int, 100)
}
request.restaurants = restaurants
<ul>
<% for(r in request.restaurants){ %>
<li>$r</li>
<% } %>
</ul>
<a href="/restaurants?cursor=${request.restaurants}">Next</>
Restarting queries automatically
Queries can run only for limited time
No problem in standard requests
Throws exception in e.g. mapper tasks
// works only for iterate method
def result = datastore.iterate {
from Restaurant
restart automatically
}
for(Entity r in restaurants){
if(!r.processed){
r.processed = true
}
r.save()
}
Saving QueryBuilder for later use
QueryBuilder builder = datastore.build {
from Restaurants
}
def results = null
int counter = 0
int curusor = null
while(!results || counter < 10){
counter++
try {
if(cursor){
builder.startAt cursor
}
results = builder.execute()
} catch(e) {
log.warning "$e"
}
}
Parent support for @Entity
@Entity class Meal {
@Parent Key restaurant
@Key String name
@Indexed Double price
@Indexed Double weight
}
Meal meal = Meal.get(['Restaurant','pakwaan'] as Key, 'chicken masala')
List<Meal> meals = Meal.findAll {
ancestor ['Restaurant','pakwaan'] as Key
where weight > 200
}
Meal.delete(['Restaurant','pakwaan'] as Key, 'chicken masala')
@Parent works for POGO coercion too
Better coercion performance
New
DatastoreEntity
interface to skip relfectionImplemented by @Entity classes automatically
Implement manually if you want performance boost
@Entity class Person { String firstName String lastName Date born Double height }
1000.times{ (person as Entity) as Person }
coercion without DatastoreEntity interface ~ 1 s
coercion with DatastoreEntity interface ~ 250 ms
(with @Version property cca 10 x slower)
Cross-group transactions
Only works on HRD
datastore.withTransaction(true){
Entity payment = new Entity('Payment')
payment.amount = price
Key paymentKey = payment.save()
Entity order = new Entity('Order')
order.payment = paymentKey.id
order.save()
}
Minor updates
Ignoring static properties
class POGO implements Serializable {
// @Ignore no longer needed
static final long serialVersionUID = 1L
}
Gracefull unindexed accessor
entity.name = 'Vladimir' // set indexed property
println entity.unindexed.name // no longer throws exception
Routes
Optional routes variables
Routes indexes
Optional routes variables
get '/posts/@year?/@month?/@day?/page-@page?', forward: '/posts.groovy'
Expands to
get '/posts/@year/@month/@day/page-@page', forward: '/posts.groovy?year=@year&month=@month&day=@day&page=@page'
get '/posts/@year/@month/page-@page', forward: '/posts.groovy?year=@year&month=@month&page=@page'
get '/posts/@year/page-@page', forward: '/posts.groovy?year=@year&page=@page'
get '/posts/page-@page', forward: '/posts.groovy?page=@page'
// to be continued
Optional routes variables
get '/posts/@year/@month/@day', forward: '/posts.groovy?year=@year&month=@month&day=@day'
get '/posts/@year/@month', forward: '/posts.groovy?year=@year&month=@month'
get '/posts/@year', forward: '/posts.groovy?year=@year'
get '/posts', forward: '/posts.groovy'
Routes indexes
get '/home', forward: 'home.groovy' // index: 0
get '/about', forward: 'about.groovy' // index: 1
...
// new notation for extension matching
// these routes will be evaluated first
all '/**/*.gtpl', ignore: true, index: -101
all '/**/*.groovy', ignore: true, index: -100
Plugins
Handling groovlet's return value
Prioritize plugin's routes
Handling groovlet's return value
after {
if(result instanceof Map){
result.each{ k,v ->
request[k] = v
}
}
}
after {
if(request.renderAsJson) {
response.contentType = 'application/json'
if(result instanceof Map && result?.status) {
response.status = result.remove('status')
}
JsonBuilder json = new JsonBuilder()
json result
json.writeTo(response.writer)
}
}
Handling groovlet's return value
if(!users.userLoggedIn){
return [error: 'You must be logged in first', status: 401]
}
if(!users.userAdmin){
return [error: 'You must be admin!', status: 401]
}
[result: datastore.execute{
from Stuff
where owner == user
}]
Prioritize plugin's routes
Some aplications may override plugin's routes
get "/@username", forward: "/profile.groovy?username=@username"
So you can move the plugin's routes before the apps one
startRoutingAt -30000 get "/_ah/gaelyk-console/",
forward: "/org/gaelyk/console/script.gtpl" // index: -30000
get "/_ah/gaelyk-console/render/",
forward: "/org/gaelyk/console/template.gtpl" // index: -29999
Parameter handling
Safe as Type conversion
// ?tag=one&tag=two assert params.tag.collect { it } == ['one', 'two']
// ?tag=one assert params.tag.collect { it } == ['o', 'n', 'e']
// ?limit=100 assert params.limit as int == 100
// ?limit=100&limit=200 try { params.limit as int assert false } catch(ClassCastException e) { // cannot cast String[] to int assert true }
Parameter handling
Safe as Type conversion
// ?tag=one&tag=two assert (params.tag as String[]).collect { it } == ['one', 'two']
// ?tag=one assert (params.tag as String[]).collect { it } == ['one']
// ?limit=100 assert params.limit as int == 100
// ?limit=100&limit=200 assert params.limit as int == 100
Thank you
Vladimír Oraný
What is new in Gaelyk 2.0
By musketyr
What is new in Gaelyk 2.0
- 5,558