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
-
@CompileStaticwherever possible
// use(GaelykCategory){ // no longer compileMyPogo mypogo = new MyPogo(a: 'a', b: 'b', c: 'c')Entity entity = mypogo as Entityentity.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 backget '/agent/**/@id', forward: '/agent.groovy?id=@id'
Minor API Changes
LifecycleManger shutdown hook
// use method call with closurelifecycle.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: 24averagePrice 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 = truequeryOptions.numberFoundAccuracy = 1000SortOptions.Builder sortOptions = SortOptions.newBuilder()sortOptions.limit = 1000SortExpression.Builder exp = SortExpression.newBuilder() exp.expression = 'distance(location,geopoint(50.07543,14.436936))'exp.defaultValueNumeric = 0exp.direction = SortDirection.DESCENDINGsortOptions.addSortExpression(exp)queryOptions.sortOptions = sortOptionsQuery.Builder queryBuilder = Query.newBuilder()queryBuilder.options = queryOptionsQuery query = queryBuilder.build('tags: "take away")
Search on GAE
SearchService search = SearchServiceFactory.searchServiceIndex 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)), 0number found accuracy 1000limit 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)), 0number found accuracy 1000limit 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 Restaurantif(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 methoddef result = datastore.iterate {from Restaurantrestart 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 = nullint counter = 0int curusor = nullwhile(!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 Keywhere 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 = priceKey paymentKey = payment.save()Entity order = new Entity('Order')order.payment = paymentKey.idorder.save()}
Minor updates
Ignoring static properties
class POGO implements Serializable {// @Ignore no longer neededstatic final long serialVersionUID = 1L}
Gracefull unindexed accessor
entity.name = 'Vladimir' // set indexed propertyprintln 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: 0get '/about', forward: 'about.groovy' // index: 1...// new notation for extension matching// these routes will be evaluated firstall '/**/*.gtpl', ignore: true, index: -101all '/**/*.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 Stuffwhere 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: -30000get "/_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,919
