|
| Common name | Value class | PAttribute constant | Stored object |
| Boolean | PBoolean | BOOLEAN | java.lang.Boolean |
| Class | PClass | CLASS | java.lang.Class |
| Currency | PCurrency | CURRENCY | java.util.Currency |
| Date | PLocalDate | DATE | org.joda.time.LocalDate |
| LocalDateTime | PLocalDateTime | DATE_TIME | org.joda.time.LocalDateTime |
| Double | PDouble | DOUBLE | java.lang.Double |
| Float | PFloat | FLOAT | java.lang.Float |
| Integer | PInteger | INTEGER | java.lang.Integer |
| Locale | PLocale | LOCALE | java.util.Locale |
| Long | PLong | LONG | java.lang.Long |
| Serializable | PSerializable | SERIALIZABLE | java.io.Serializable |
| String | PString | STRING | java.lang.String |
| Text | PText | TEXT | java.lang.String, saved using the Hibernate type text |
| Time | PLocalTime | TIME | org.joda.time.LocalTime |
| Reference | PRef | REFERENCE | org.tinymarbles.model.PObject |
| Set | PSet | SET | java.util.Set<PObject> |
Notice that the two last types, reference and set, allow composition to take place. All the 16 types above have corresponding constants in the class PAttribute, so you can very quickly add it to a type with the following construct:
PType type = ...;
PAttribute att = type.putAttribute("name", PAttribute.STRING);
you can also use the put() method that returns the PType instance, so you can chain calls:
type.put("active", PAttribute.BOOLEAN)
.put("created", PAttribute.DATE_TIME);
You can put attributes to a PType at any time. Even after you have many instances, you can still add new attributes to a type.
If you put the same attribute on a type again, it checks the value class of the attribute: if the existing attribute has exactly the same value class as the argument of put(), the call is ignored. If the value class doesn't match, a recoverable TypeMismatchException is thrown (recoverable in the sense that the transaction is not lost. See chapter 4 ).
5.1.1. Date and Time using Joda TimeTiny Marbles never exposes java.util.Date or java.util.Calendar instances. We use Joda-Time instead, because it provides immutable objects and a concise and comprehensive API to transform and make calculations with dates. We hope you will also find it useful.
The attribute type LocalDate stores a date ignoring the time of day. The value is translated to a SQL DATE column (or TemporalType.DATE in the EJB3 specification). We also provide a LocalTime attribute that is stored as SQL type TIME (or TemporalType.TIME in the EJB3 specification). Finally, there's the LocalDateTime attribute that stores a date plus a time, with milissecond precision. To achieve this precision across different databases, we store it as a Long.
5.1.2. Serializable valuesYou can persist any value that implements java.io.Serializable. If you do it, you must override the equals() and hashCode() methods, or update operations might not work. We strongly recommend that you only store immutable, value-equivalent serializables.
For example, consider storing a Point serializable in a marbles object:
public class Point implements java.io.Serializable {
private int x;
private int y;
// getters and setters
@Override
public int hashCode() {
return this.getX() & this.getY();
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj instanceof Point) {
Point other = (Point) obj;
return (this.getX() == other.getX()) && (this.getY() == other.getY());
}
return false;
}
}
The code below works too:
PObject rect = repository.load(rectId);
Point point = rect.get("topLeft");
point.setX(45); // use as you would
// entirely new instances also work
rect.set("bottomRight", new Point(100, 50));
5.2. Reserved attribute namesYou cannot use the following names for dynamic attributes:
childrenidparentsystemIdtypeThese names would be shadowed by the regular getters (and setters) in the PObject class. To avoid frustrating debugging sessions, Marbles will throw an IllegalAttributeDefinitionException when an attribute is created with a reserved name:
// will throw IllegalAttributeDefinitionException:
type.put("parent", PAttribute.REF);
5.3. Object creationNew object creation follows the same pattern. But instead of asking the repository for a new object, you ask its type. You can also create the object with a System Id, a unique name that is known through the whole application:
PType type = repository.loadType("MyType");
PObject obj = type.newInstance("unique name");
obj.set("name", "A name")
.set("active", Boolean.FALSE)
.set("created", new LocalDateTime());
if (obj.hasAttribute("owner") && ownerId != null) {
PObject owner = repository.load(ownerId);
obj.set("owner", owner);
}
obj.save();
The call to save() persists the object for the first time.
Since release 1.0rc1, the call to save() is entirely optional. New instances will be persisted when the repository is committed. Notice that the PObject's id property is auto-generated. If you need to know it before commit, you must call save() manually. The following code also works:
PObject obj = type.newInstance();
repo.commit();
Long id = obj.getId(); // is not null
Objects loaded by the repository also don't require a call to save():
repository.loadType("MyType")
.put("owner", PAttribute.REFERENCE);
repository.commit();
The relation PObject – PType is bidirectional. Supposing you have an instance of "MyType" in the variable obj, you could create a second instance like this:
// create a second object of the same type, without system Id:
PObject second = obj.getType().newInstance();
second.set("name", "second")
.set("active", Boolean.TRUE)
.set("created", new LocalDateTime())
// use the same owner of the first object
.set("owner", obj.get("owner"))
repository.commit();
5.4. Working with collectionsSets can also be accessed the same way simple attributes are accessed. Supposing we have an object with an attribute friends of type SET, we can get the set and manipulate it as we wish:
PObject obj = repository.load(objId);
// get the set just like any attribute
Set<PObject> friends = obj.get("friends");
// change the contents of the set
PObject aFriend = repository.load(friendId);
friends.add(aFriend);
Since this construction is often too complicated, we added a shortcut:
obj.add("friends", aFriend);
5.4.1. Bidirectional sugarVery often objects have a bidirectional relationship, either one-to-many or many-to-many. If you want to update the inverse relationship side directly, you can use the alternative version of the add/remove methods that take a second string. Below is an example of one-to-many relationship between container and elements:
PObject container = ...;
List<PObject> elements = ...;
for (PObject e : elements) {
container.add("elements", e, "container");
}
assertEquals(container, e.get("container"));
5.5. Tree structuresIf your data model requires a tree structure, you can implement it directly on the PObjects:
PObject root = repository.load("RootCatalog");
PObject catalog = root.getType().newInstance();
catalog.setParent(root);
catalog.set("name", ...); // set other attributes
Now the catalog root has an extra child. It also works from the other end:
root.addChild(catalog);
A PObject instance can have at most one parent, and Tiny Marbles doesn't check if you are creating a cycle. You can check it yourself by calling:
boolean v = catalog.hasAncestor(root);
The reason for the lack of automatic checking is that relational databases and SQL are terrible at handling tree operations such as list all ancestors of the node N. This structure can also be slow if you load large portions of the tree at once.
In case you need very often to traverse a tree up to its root, we recommend that you use references and sets to cache this kind of information in the object:
PObject obj = type.newInstance();
obj.setParent(parent);
obj.get("ancestors").addAll(parent.get("ancestors"));
obj.add("ancestors", parent);
Beware of premature optimizations, though. Loading an object is pretty cheap, but loading all its dynamic attributes adds one extra database access, and loading the contents of the persistent set is yet another database hit. Tiny Marbles leverages on the second-level Hibernate cache to amortize this effect, but you might end up with a hight memory footprint needlessly.
Never forget that you can deal with it once the situation arrives. The whole point of having a dynamic model is being able to change it on demand.
5.5.1. fast loopingJava 5 introduced the enhanced for loop, which uses an iterator internally to loop through a collection. When looping through a node's children, you might be tempted to do it like this:
for (PObject child : root.getChildren()) {
if (some condition) {
// throws ConcurrentModificationException:
child.setParent(null);
}
}
You'll have an unpleasant surprise. Since the parent-child relationship is bidirectional, the child node will remove itself from the collection you're iterating through, causing a ConcurrentModificationException. If you want fast looping, you can use the root's listChildren() instead:
for (PObject child : root.listChildren()) {
if (some condition) {
// will work:
child.setParent(null);
}
}
The collection returned by listChildren() is a copy of the original collection. Changes done to the copy do not fall through to the original set.
6. QueriesWhen we talk about objects in a repository, we say we use a filter to fetch a subset of the data in the repository. In practice, the difference between "querying" and "filtering" when you only think about a bunch of objects is irrelevant.
Filters are created by the repository. You need to provide a PType instance, or its name:
Filter filter = repository.createFilter("MyType");
List<PObject> list = filter.list();
The code above will retrieve all objects of the type MyType. You can narrow your search adding restrictions on the object's properties, much like Hibernate's Criteria API.
You can also set the first result and the maximum number of results returned, if you don't set any parameter, it will return all results matching the criteria, see example below:
// will return the max 20 results
List<PObject> list = repository.createFilter("MyType")
.list(20);
// will return also max 20 results but will start from the 3rd result found
List<PObject> list = repository.createFilter("MyType")
.list(3,20);
6.1. Simple RestrictionsYou can add restrictions to the filter, using the names of the object's attributes:
filter.eq("name", "Tiago");
After this call, the filter will only retrieve objects which have the attribute name set to "Tiago". The method also returns the same Filter instance, so you can chain calls:
List<PObject> list = repository.createFilter("MyType")
.eq("age", 26)
.eq("balance", -40.00f)
.list();
Most attributes can be restricted with greater than, lesser or equal, or similar restrictions. Below is a query that gets the People with age below eighteen and equal or above 13:
List<PObject> teens = repository.createFilter("People")
.lt("age", 18)
.ge("age", 13)
.list();
6.2. Date and Time shortcutsFor LocalDate, LocalDateTime and LocalTime, we also have aliases for gt and lt:
List<PObject> ordersInAugust = repository.createFilter("Order")
// after july 31st
.after("date", new LocalDate(2005, 07, 31))
// before september 1st
.before("date", new LocalDate(2005, 09, 01))
.list();
The same query can be rewritten to be even simpler (notice that between is inclusive, so we changed the dates):
List<PObject> ordersInAugust = repository.createFilter("Order")
.between("date", new LocalDate(2005, 08, 1),
new LocalDate(2005, 08, 31)
)
.list();
6.3. String restriction with LIKEFor strings you can also use the like and ilike filters, similar to the SQL like and ilike. You can use these filter to compare string properties with patterns using the metacharacters % and _
For example like("_at") would return either "bat", "cat", "fat". And like("% cat") would return any strings that ends with the word, including "Black cat" but not "pussycat". The code below returns all students with first name "John".
List<PObject> johns = repository.createFilter("Students")
.like("fullname", "John %")
.list();
Note that like is case-sensitive if and only if the operator LIKE is case-sensitive on the underlying database. MySQL, for example, only provides a case-insensitive LIKE. You can use the ilike filter for a case-insensitive version of like:
List<PObject> johns = repository.createFilter("Students")
.ilike("fullname", "John%")
.list();
The query above will return students with first name "John", "JOHN", "JoHnAtan", etc.
6.4. References and SetsYou can also pass instances of PObject if the attribute is of type reference (see all attribute types). The following query gets the cats that have the mother instance as value of the property "mother":
PObject mother = repository.load(motherId);
List<PObject> kittens = repository.createFilter("Cat")
.eq("mother", mother)
.list();
Attributes of type Set can also be restricted using empty/non-empty clauses, contains clauses and also be restricted by its size. The following filter fetches articles that have at least one comment, for which one of the authors is the passed object:
PObject author = ...;
List<PObject> articles = repository.createFilter("Article")
.isNotEmpty("comments")
.contains("authors", author)
.list();
Additionally, you can add restrictions on the size of the collection. Let's say we want articles with more than five comments but no more than twenty:
List<PObject> articles = repository.createFilter("Article")
.sizeGt("comments", 5)
.sizeLe("comments", 20)
.list();
6.4.1. The subtle difference between null and emptyIf you want to test if a simple value is not set, you can use the isNull restriction:
filter.isNull("prop");
If prop is a persistent collection, this is always false, and your filter will return no results. Conversely, if you restrict a set with isNotNull, all objects will pass. You should use isEmpty instead:
List<PObject> parents = repository.createFilter("Cat")
.isNotEmpty("kittens")
.list();
6.5. Collection restrictionsFor all attribute types except Sets, you can create a restriction by passing the name of the attribute and a Collection or array of values to the in filter. This filter returns only objects whose attribute is contained in the given list. The code below returns all tourists from Germany and Brazil:
List<PObject> tourists = repository.createFilter("tourists")
.in("country", new Object[] { "Germany", "Brazil" } )
.list();
Note that it works for reference attributes too. We can use the results of the query above in another query:
List<PObject> museums = repository.createFilter("museums")
.in("visitors", tourists)
.list();
6.5.1. Id collectionsThere are two extra methods that handle the PObject's ids:
Collection<Long> idList = ...;
Collection<String > sysIdList = ...;
List<PObject> tourists = repository.createFilter("tourists")
.idIn(idList)
.systemIdIn(sysIdList)
.list();
6.6. Tree node restrictionsYou can restrict objects by their parent node:
PObject group = ...; // load a group
List<PObject> parentGroups = repository.createFilter(group.getType())
.parent(group)
.eq("active", true)
.list();
Since each object can have at most one parent, there's no filter by child. Once you know the child, use child.getParent() for that. More details on the documentation for attributes.
To fetch all nodes without a parent (also known as root nodes), you can give null to the parent filter:
List<PObject> rootGroups = repository.createFilter("Group")
.parent(null)
.list();
6.7. Serializable attributesNotice that Serializable objects are not searchable by Hibernate, which means you will only be able to use them in equality comparisons and nullability checks. Suppose we store serializable Point instances in a type:
PType rectangle = repository.createType("Rectangle")
.put("topLeft", PAttribute.SERIALIZABLE)
.put("bottomRight", PAttribute.SERIALIZABLE);
A filter with equality restrictions works:
filter.eq("topLeft", new Point(14, 20));
Other restrictions don't work:
filter.ge("topLeft", new Point(0, 0));
Please refer to the Hibernate documentation for more details.
6.8. Combining filtersYou can modify the results shown by a filter using other filters. Currently, we provide three operations.
Filter.and(Filter) – restricts the results returned by this filter to the results returned by the argument filter.Filter.or(Filter) – adds to the results returned by this filter all results returned by the other filter.Filter.not(filter) – excludes from the results returned by this filter all results returned by the argument filter.Notice that filters don't have to agree in anything. The following filter will return all jobs with name ending in "ary" and all toys suitable for use in kindergarten:
Filter filter = repo.createFilter("Jobs")
.like("name", "%ary")
.or(repo.createFilter("Toys").le("minimumAge", 4));
6.8.1. Ordering of calls is irrelevantSince filter combination is just another restriction you're adding to a filter, which restrictions you add before or after the call is irrelevant.
Filter filter = repo.createFilter("Toys")
.eq("origin", "Holland")
.or(repo.createFilter("Toys")
.eq("year", 2000)
)
.lt("price", 500.00f);
The filter above returns objects that match the expression:
(obj.type = "Toys" and obj.origin = "Holland" and obj.price < 500)
union
(obj.type = "Toys" and obj.year = 200)
To obtain the results where everybody is restricted by price, you can use:
Filter filter = repo.createFilter("Toys")
.lt("price", 500.00f)
.and(repo.createFilter("Toys")
.eq("origin", "Holland")
.or(repo.createFilter("Toys")
.eq("year", 2000)
)
);
Future versions of Tiny Marbles will bring new additions to this field too, until we can satisfy all querying needs with readable code that's easy to write and to read, without becoming inefficient.
6.9. SortingYou can sort results by their attributes:
Filter filter = repo.createFilter("Toys")
.lt("price", 600.0f)
.eq("origin", "Holland")
.sortAsc("price")
.sortDesc("year");
The filter above will show you all dutch toys with price under 600, sorted first by price ascending then by year descending.
When sorting, null values are considered to come after non-null values (but before them when sorting in descending order).
6.9.1. Special handling: id and systemIdThe keywords id and systemId are specially handled and do not refer to dynamic attributes, but to the properties of the PObject (i.e. PObject.getId() and PObject.getSystemId()).
filter.sortAsc("id").sortDesc("systemId");
Will sort by the object's persistent id ascending, then by the systemId descending.
6.9.2. Special handling: parent and reference attributesYou can also sort by the value of properties from the parent node and/or referenced objects. Consider the example below:
filter.sortAsc("parent.age");
Given two results, A and B, we can build the table below (NT means not tested):
| A.parent | B.parent | A.parent.age | B.parent.age | resulting order |
| null | null | NT | NT | not changed |
| Pa | null | NT | NT | A, B |
| null | Pb | NT | NT | B, A |
| Pa | Pb | 30 | 40 | A, B |
| Pa | Pb | 35 | 22 | B, A |
| Pa | Pb | 90 | 90 | not changed |
The same applies to reference properties.
6.10. Advanced FiltersIf you need a complex query which you can't do with the filters of Tiny Marbles, you better use the advanced filters. Here you can pass SQL or HQL directly to the database, for example for calculation purposes.
// using SQL
StringBuilder queryString = new StringBuilder("select sum(v.v_int) as total ");
queryString.append("from PObject o ");
queryString.append("left outer join PValue v on o.id=v.owner_id and v.name=:vName");
Map<String,Object> values = new HashMap<String, Object>();
values.put("vName", "number");
SQLFilter filter = getCurrentRepository().createSQLFilter(queryString.toString(), values);
List results = filter.list();
//using HQL
StringBuilder queryString = new StringBuilder("select avg(v.integerValue) as average ");
queryString.append("from PObject o ");
queryString.append("left outer join o.values v with v.name=:attributeName");
Map<String,Object> values = new HashMap<String, Object>();
values.put("attributeName", "number");
SQLFilter filter = getCurrentRepository().createHQLFilter(queryString.toString(), values);
List results = filter.list();
Please send us comments, questions, criticism!