Garfa - is Groovy ActiveRecord for Google Appengine
It's a tiny wrapper around Objectify 4, and should work with any Groovy project on Appengine. It's pretty safe to use Garfa in your project, because all underlying work is done by Objectify, and if you have something very specific you could always dig down to raw Objectify.
Garfa extends your database models with methods for querying, storing and updating models for Google Appengine Datastore.
<dependency>
<groupId>com.the6hours</groupId>
<artifactId>garfa</artifactId>
<version>0.7</version>
</dependency>
<repositories>
<repository>
<id>the6hours-release</id>
<url>http://maven.the6hours.com/release</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
https://github.com/splix/garfa/
Project is licensed under Apache 2 license.
You need to register classes through Garfa on app start.
ObjectifyFactory objectifyFactory = //... you need to have Objectify already configured there
Garfa garfa = new Garfa(objectifyFactory)
// Car and Dealer is our models
List<Class> models = [Car, Dealer]
// add magic to our models
garfa.register(models)
If you have a Spring Framework app, you could easily initialize Objectify and Garfa with your @Configuration class, like:
@Configuration
class StorageConfig {
@Bean
ObjectifyFactory getObjectifyFactory() {
ObjectifyFactory objectifyFactory = new ObjectifyFactory()
Garfa garfa = new Garfa(objectifyFactory)
def models = [
Car,
Dealer
]
models.each { Class clz ->
objectifyFactory.register(clz) // register with Objectify
garfa.register(clz) // register with Garfa
}
return objectifyFactory
}
}
@Entity
class CarModel {
@Id
Long id
@Index
String vendor
@Index
String model
@Index
int year
void beforeInsert() {
if year == 0 {
year = 1896
}
}
}
@Entity
class Car {
@Id
Long id
@Parent
Key<CarModel> model
@Index
int price
@Index
String color
}
CarModel mustang = new CarModel(vendor: 'Ford', model: 'Mustang', year: 2012)
mustang.save(flush: true) //sync save
Car redMustang = new Car(model: mustang.key, price: 22000, color: 'red')
redMustang.save() //async save
Current instance of entity have .update
method, that accept a Closure that will update db. Notice, that this
method will load a fresh version from DB, and try to update. Also it will try to perform this operation up to 3 times
if fail (for the situation when you're updating same entity from different threads)
Ok, lets make a discount!!! $22000 -> $21000:
Car withDiscount = redMustang.update {
price = 21000
}
.update
method also return updated instance. Or throw exception if failed to update (only when all
3 tries are failed).
Car blackMustang = Car.findFirstByModelAndColor(mustang.key, 'black')
// load model with ID 5161
CarModel foo = CarModel.load(5161)
List<Car> allYellow = Car.findAllByColor('yellow')
Model
, Color
, etc is a entity fields to filter.
There is two method for loading data from database:
Will throw error if there is no entity with specified ID
Long id = 1
try {
Car car = Car.get(id)
} catch (NotFoundException e) {
...
}
Key<Car> carKey = new Key<Car>(Car, 1)
try {
Car car2 = Car.get(carKey)
} catch (NotFoundException e) {
...
}
Will returns null
if there are no entity with specified ID.
Long id = 1
Car car = Car.load(id)
if (car == null) {
... not found
}
Key<Car> carKey = new Key<Car>(Car, 1)
Car car2 = Car.load(carKey)
if (car2 == null) {
... not found
}
Loads list of entities for specified ids:
List<Car> cars = Car.getAll([1, 2, 3])
You could get a Objectify Query for a model:
Query<Model> query = Model.queryWhere([<fields>], [<params>])
where:
[model: 'Ford']
or ['model =': 'Ford']
or ['count >': 5]
.[limit: 4]
or [order: '-count']
There is an another method for querying:
Clazz.findWhere([<fields>], [<params>]) {
// code executed against Query
}
where:
[model: 'Ford']
or ['model =': 'Ford']
or ['count >': 5]
. First two are
equal filters[limit: 4]
or [order: '-count']
Car.findWhere([], []) { limit(5) }
(btw, it's the same as .findWhere([], [limit: 5])
)Possible query parameters:
* limit
* offset
* ancestor - key or parent entity
* order - in format [order: 'model']
standard ascending order, or [order: '-year']
for descending order
* cursor - web-safe string for cursor, or com.google.appengine.api.datastore.Cursor
instance
For example:
//get maximum 20 cars where count > 10, ordered by count field, descending
List cars = Car.findWhere(['count >': 10], [order: '-count', limit: 20])
//a parent instance
CarModel fordFocus
//find by parent:
List<Car> cars = Car.findByAncestor(fordFocus)
//or by a parent key:
List<Car> cars = Car.findByAncestor(fordFocus.key)
Use .save()
method
car = new Car(
brand: 'Ford',
model: 'Mustang',
count: 0
)
car.save()
GAE uses optimistic-locking transactions, so, to update an item, Garfa tries to load fresh instance from DB and execute your code against this instance.
If save of update instance is failed, Garfa retries this steps again, at least 3 times.
car.update { Car loaded ->
loaded.count++
}
Car.update idOrKey, { Car loaded ->
loaded.count++
}
Where:
car
- current instanceidOrKey
- id or Key of instance to updateloaded
- instance loaded for updateJust use .delete()
method:
Car car = Car.get(15)
car.delete()
Garfa supports dynamic finders like:
Car.findByModelAndYear("Mustang", 2008)
Return list of entries, filtered by specified fields
Options is a optional argument, it's a Map
with following possible entries:
limit
- max number of elements, like [limit: 2]
offset
- initial offset, like [offset: 10]
sort
- sort by field value. Value of this option is a field name to use sort for sorting. By default
it sorts in ascending order, to use descending use -
as a prefix to field name.
Like [sort: 'model
], [sort: '-year']
cursor
- string value or Cursor
instance to use for this querySame as findBy
, but returns first element only. Or null
if not found.
Car car1 = Car.findFirstByVendor('Vaz')
Car cheapFord = Car.findFirstByVendor('Ford', [sort: 'price'])
List<Car> allFords = Car.findByVendor('Ford')
List<Car> firstPageFords = Car.findByVendor('Ford', [limit: 10])
You have direct access to Objectify's Query, by using two following methods:
.findFirst {}
.findAll {}
where you can pass the code that can modify any options of passed Query object. Please notice, that query instance, it's no a passed parameter, your code will operates directly against query instance, as an DSL.
For example:
Car.findAll {
filter('vendor =', 'Ford')
limit(10)
}
Use method .withObjectify {}
of a model, this Closure will be called agains Objectify instance, so you can
do whatever you want:
Key key = ....
boolean loaded = CarModel.withObjectify {
//all methods here are delegated directly to Objectify instance
isLoaded(key)
}
TO DO
Called on both insert and update
class Car {
void beforeSave() {
...
}
}
Called before first save (when Id is null)
class Car {
void beforeInsert() {
...
}
}
Called before object update
class Car {
void beforeUpdate() {
...
}
}