Tiny Marbles documentation

1. What is Tiny Marbles?

Tiny Marbles provides persistence for dynamic objects, through a data model that you can create, modify and extend at runtime. It is useful for developers who need to create rapid prototypes with an unknown model and developers of applications that have inherently dynamic data models. It runs under Java 1.5, supports any database that Hibernate supports.

Take our Quick Start for an overview.

TOP

1.1. Our mission

Tiny Marbles is a transactional, persistent object repository for dynamic objects. But what does it mean?

  • Being a repository means it is a service (as opposed to framework). No code generation is used, no special bindings are used. Apart from the initial setup (which involves a Hibernate schema export), all the interaction between Tiny Marbles and the application is done programatically, at runtime.
  • Being persistent means the user can shut it down and find the data after the next startup, provided he reveals the intention to keep it.
  • Being transactional implies a certain degree of isolation between operations. The exact isolation level is a Hibernate configuration and can be fine tuned. A transactional repository performs small ACID operations and provides a mechanism for transaction demarcation.
  • It is an object repository, which means it stores objects. The objects are also dynamic in the sense that their structure can be modified after their creation.

Additional restrictions were placed on the design of Tiny Marbles: it must be fun to use, fill a real need, be robust and empowering.

TOP

1.2. Improving communication

It is fun and engaging. This requirements reflects our CommWorld culture: we value high productivity, agile development and management, and prototyping with short release cycles.

It also needs to be easy to learn. Marbles provides a very low cognitive overhead in application code, making it look more meaningful. A look at the quick start confirms that.

We believe people need to give new meaning to old structures, combine ideas, and remix. Data stored in a Marbles repository can be reinterpreted in different contexts – several applications can be combined through their data structures to create new applications. This allows conversations to take place at many levels of abstraction: we can talk about persistent objects, or we can talk about person objects, or even things with an attribute called "enabled".

"In the beginner's mind there are many possibilities, but in the expert's there are few." — Shunryu Suzuki

We want beginners and experts to be able to talk to each other meaningfully.

TOP

1.3. Robustness

Another major requirement on the design of Tiny Marbles was that it becomes robust and resilient. We want it to reduce the need to test all our code over and over again.

Instead of trying to get all bugs covered, we want to avoid bug introduction altogether.

Instead of providing a lot of functionality, we will develop Tiny Marbles so that it works like a sharp knife. Sure, one can't use a knife for all tasks, but it works exactly as expected for the right tasks, and with a minimum of effort.

A minimum of effort also reflects on the performance. If we know that our knife is only used to cut, we can easily tell where the development energy must go, what kind of optimizations are useful, and so on.

And to ensure that our knife is always sharp and shiny, all important and critical parts of Tiny Marbles code are covered by an extensive suite of test. With the help of a test coverage report generated by Cobertura, we can keep track of every single line of Tiny Marbles.

Those aspects increases the quality of both Tiny Marbles and applications build on top of it. High quality, resilient apps are easier to maintain, improve and modify.

TOP

1.4. A tool to trust

When we put fun and robustness together, we achieve the ultimate goals that are driving this whole story: we want to quickly build software at a relatively low cost without stress. We also want to choose the work we do, and knowing and trusting our tools is fundamental for that. We try our best so Marbles does the same for you.

2. Getting Tiny Marbles

If you're developing with Tiny Marbles, you need to fetch the JAR and the dependencies. The easiest way to do that is with Ivy. We have our own Ivy repository at http://www.os-kit.org/ivyrep/, and you can use our ivy configuration file or merge it with your own. If you are using Eclipse, we recommend that you use the IvyDE Plugin. If not, we recommend that you learn how to use Ivy with Ant. The obvious benefit of Ivy is that if we release a new version of Tiny Marbles, you'll only need to change one configuration file to get the new JAR and any other libraries we might have updated.

You can always download from sourceforge. The *-bin* distribution contains the tiny marbles JAR, all dependencies and the javadocs.

TOP

2.1. Building from the sources

Tiny Marbles uses Ivy to manage the dependencies. All the dependencies for the release ${app.version} can be seen in the Ivy report.

You can download the source distribution or check out the files from the CVS using the following command:

cvs -z3
  -d:pserver:anonymous@tinymarbles.cvs.sourceforge.net:/cvsroot/tinymarbles
  co -P
  tinymarbles

If you plan to build it with Ant, you have to add the Ivy JAR to your classpath. Usually, it's best to put it in the ANT_HOME/lib directory. You might also need the JUnit jar there.

After that, just call ant or ant build on the root build directory and it will download all the dependencies, compile, test and generate the Tiny Marbles jar.

If you are using Eclipse, we recommend that you use the IvyDE Plugin. You must point the configuration to /tinymarbles/ivyconf.xml. Eclipse needs two configurations, called default and build. You can right-click the ivy.xml library and then "Configure...", and you'll get this dialog. The docs configuration is only used to generate the website files, so you won't need it for development. If you check out the project from CVS, you also get the project configuration, classpath, etc.

TOP

3. Configuring your environment

You need to configure the Hibernate access, as per the Hibernate configuration documentation. The mappings are configured in the hibernate.cfg.xml file. This is how it looks like on version ${app.version}:

< !— hibernate.cfg.xml —>
<!DOCTYPE hibernate-configuration PUBLIC
  "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <mapping class="org.tinymarbles.model.PObject"/>
    <mapping class="org.tinymarbles.model.PType"/>
    <mapping class="org.tinymarbles.model.PAttribute"/>
    <mapping class="org.tinymarbles.model.PValue"/>
    <mapping class="org.tinymarbles.model.value.PInteger"/>
    <mapping class="org.tinymarbles.model.value.PString"/>
    <mapping class="org.tinymarbles.model.value.PLocalDateTime"/>
    <mapping class="org.tinymarbles.model.value.PClass"/>
    <mapping class="org.tinymarbles.model.value.PBoolean"/>
    <mapping class="org.tinymarbles.model.value.PLong"/>
    <mapping class="org.tinymarbles.model.value.PDouble"/>
    <mapping class="org.tinymarbles.model.value.PFloat"/>
    <mapping class="org.tinymarbles.model.value.PCurrency"/>
    <mapping class="org.tinymarbles.model.value.PLocalDate"/>
    <mapping class="org.tinymarbles.model.value.PLocalTime"/>
    <mapping class="org.tinymarbles.model.value.PLocale"/>
    <mapping class="org.tinymarbles.model.value.PRef"/>
    <mapping class="org.tinymarbles.model.value.PSet"/>
    <mapping class="org.tinymarbles.model.value.PText"/>
    <mapping class="org.tinymarbles.model.value.PSerializable"/>
    <event type="flush-entity">
      <listener class="org.tinymarbles.impl.hibernate.FlushEntityListener"/>
      <listener class="org.hibernate.event.def.DefaultFlushEntityEventListener"/>
    </event>
  </session-factory>
</hibernate-configuration>

Although you can put additional Hibernate configuration in the XML file, we recommend that you configure the database using a hibernate.properties file. Refer to the Hibernate documentation, Chapter 3 – Configuration to learn what properties you need. The binary distribution also includes a properties file you can adapt to your needs.

You should to put hibernate.properties and hibernate.cfg.xml in your classpath, so you can instantiate a RepositoryFatory with the following code:

RepositoryFactory factory = RepositoryFactory.createDefault();

TOP

3.1. Using the Spring container

If you want complete control about the instantiation of the Hibernate SessionFactory, you can use an IoC container such as Spring. Your bean definition file should contain something like this:

<bean class="org.tinymarbles.RepositoryFactory"
  id="RepositoryFactory" >
  <constructor-arg ref="ConversationFactory"/>
</bean>
 
<bean class="org.tinymarbles.impl.hibernate.HibernateConversationFactory"
  id="ConversationFactory" >
  <constructor-arg ref="SessionFactory"/>
</bean>

Then you configure the SessionFactory as you wish. If you want, you can also configure the Repository bean like this:

<bean class="org.tinymarbles.Repository"
    name="Repository"
    factory-bean="RepositoryFactory"
    factory-method="open"
    singleton="false" />

This would allow you to inject a fresh Repository instance in your own application beans.

TOP

4. Accessing the repository

You work with the persistent data by means of a Repository instance. All database transaction management is automatically handled by Marbles, so you can use the repository as you would access an input stream:

Repository repo = factory.open();
// ... do stuff
repo.close();

While a repository is alive, you can populate it with data and read it back. If something goes wrong during database access, the cleanup is automatic: any ongoing transactions will be rolled back, the Hibernate session will be closed, and the repository instance will be closed again. Once it's closed, a Repository cannot be reopened. You will need a fresh instance from the factory.

Additionally, the close() operation is idempotent, meaning you can call it repeatedly with the same effect as calling it only once. This allows the following code to work:

Repository repo = factory.open();
try {
  repo.load((Long)null);
}
finally {
  repo.close();
}

The code above always throws an exception, because you can't load an object passing a null persistent id. When the exception is generated, the repository executes the cleanup procedure and closes. The call inside the finally block will ask to close a repository that's already closed. Nothing will be performed, no exception will be thrown, and you can be certain that the exception generated by the load() call will go up the stack.

TOP

4.1. Applying changes

While you access repository data, you'll be working in a transactional environment. After you're done updating the repository contents, you must ask it to make the changes permanent with a commit() call.

Repository repo = factory.open();
// ... do some changes
repo.commit();
// ... do some more reading
repo.close();

Anything changed before the commit() call will be made permanent. Anything changed after the commit() will be discarded when the close() call executes. We believe this is pretty straightforward.

Nothing prevents you from calling commit() several times on the same repository. Each time you do, the contents will be flushed to the database and the transaction will be closed; a new transaction will start automatically by the next read.

We also recommend that you use one repository instance for each unit of work.

TOP

5. Data modelling

Although Tiny Marbles allows you to forget about data types and use duck typing along the way, we must start somewhere. At any time, you can use the repository to create a new type or modify an existing type. These persistent types are represented by the class PType.

Each type consists of a collection of PAttributes, each with a name and a value class. The value class determines the kind of data that can be stored.

TOP

5.1. Attribute types (a.k.a. value classes)

The following types are supported by Tiny Marbles without special configuration.

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 Time

Tiny 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 values

You 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));

TOP

5.2. Reserved attribute names

You cannot use the following names for dynamic attributes:

  • children
  • id
  • parent
  • systemId
  • type

These 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);

TOP

5.3. Object creation

New 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 PObjectPType 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();

TOP

5.4. Working with collections

Sets 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 sugar

Very 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"));

TOP

5.5. Tree structures

If 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 looping

Java 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.

TOP

6. Queries

When 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);

TOP

6.1. Simple Restrictions

You 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();

TOP

6.2. Date and Time shortcuts

For 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();

TOP

6.3. String restriction with LIKE

For 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.

TOP

6.4. References and Sets

You 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 empty

If 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();

TOP

6.5. Collection restrictions

For 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 collections

There 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();

TOP

6.6. Tree node restrictions

You 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();

TOP

6.7. Serializable attributes

Notice 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.

TOP

6.8. Combining filters

You 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 irrelevant

Since 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.

TOP

6.9. Sorting

You 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 systemId

The 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 attributes

You 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.

TOP

6.10. Advanced Filters

If 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();

TOP

Please send us comments, questions, criticism:

Please send us comments, questions, criticism!