Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Managing Entity Relationships with Hrorm

DZone 's Guide to

Managing Entity Relationships with Hrorm

See how Hrorm handles relationships between entities.

· Database Zone ·
Free Resource

Prior articles about Hrorm gave an introduction and a first lesson. You may wish to take a look at those if you haven't before diving into this article, which will explain how Hrorm (a concise, declarative ORM library for Java) handles relationships between entities.

On Relations

SQL RDBMS platforms remain somewhat unfashionable. Some people seem to believe in a myth that they are not scalable enough. This may be true for some very large data sets and applications, but unless you work for a company with a name like Google or Amazon, your data is probably easily manageable by most popular databases.

There is also a tendency to imagine that a storage solution with an explicit schema will slow down development efforts. Many people have pointed out that you always have a schema (possibly more than one) even if you have not used tools that make it explicit. In my experience, knowing more about the systems I work on does not slow me down.

But neither of those two things is a critical issue to me. The flaw with no-SQL document stores is in how they manage relations. If you have data describing actors and movies, a document just will not suffice. If you choose to make the movies your top level structure, you will have to repeat the data about actors, since actors can appear in more than one movie. If you choose to make actors your top level, you have to repeat the data about movies, since movies have more than one actor. Normalization is (in part) the process of figuring out what your entities are, and how they relate to each other.

I know that document stores have references from one document to another, but RDBMS's have been managing data according to a well-thought out mathematical theory for decades. To ignore them feels like a step backwards to me.

It's the job of ORM tools like Hrorm to make the annoyances of using an RDBMS less. And the experience of using them (almost) as easy as using text files. Developers should get power and security, and they should not have to pay with inconvenience and aggravation. Whether or not Hrorm succeeds is up to you to judge.

Siblings

One of the simplest relations two entities can have is when one always accompanies another. In the United States, every city is in a state. In Java, we might model cities and states like this. (The examples omit getters, setters, equals, and other Java essentials.)

public class State {
    Long id;
    String name;
}

public class City {
    Long id;
    String name;
    State state;
}

The corresponding tables that would back this model would look almost identical.

CREATE TABLE STATE (
    ID INTEGER PRIMARY KEY,
    NAME TEXT
);

CREATE TABLE CITY (
    ID INTEGER PRIMARY KEY,
    NAME TEXT,
    STATE_ID INTEGER
);

The biggest difference is that in the object model, we include a reference to the state itself, in the database, the CITY only directly refers to the STATE's primary key. Hrorm works by defining the relationship between the RDBMS SQL world and the Java world by creating DaoBuilder objects. The Hrorm code to describe these entities looks like this.

DaoBuilder<State> StateDaoBuilder = new DaoBuilder<>("STATE", State::new)
        .withPrimaryKey("ID", "STATE_SEQUENCE", State::getId, State::setId)
        .withStringColumn("NAME", State::getName, State::setName);

DaoBuilder<City> CityDaoBuilder = new DaoBuilder<>("CITY", City::new)
        .withPrimaryKey("ID", "CITY_SEQUENCE", City::getId, City::setId)
        .withStringColumn("NAME", City::getName, City::setName)
        .withJoinColumn("STATE_ID", City::getState, City::setState, StateDaoBuilder);

Most of that is identical to the features of Hrorm described in the previous articles. The only new thing is the call to the method withJoinColumn. That method accepts not just the column name and the accessors of the field but also a reference to the DaoBuilder (technically, the DaoDescriptor, an interface Hrorm uses internally and that DaoBuilder implements) of the joined entity.

Now when we perform a select of a city from the database through Hrorm, it will not just read the CITY table, it will create a query that looks like this.

SELECT A.ID, A.NAME, B.ID, B.NAME
    FROM CITY A
    LEFT JOIN STATE B ON A.STATE_ID = B.ID
    WHERE ...

After issuing that query, Hrorm will automatically populate a State object and set it on the City object. Similarly, when either the Dao.insert() or Dao.update method is invoked, Hrorm will make sure that the correct reference is persisted. But, Dao.delete() will act only on the entity itself, it will not cascade to the joined entity.

Parent-Child

A join relationship only works when one object contains a reference to one other object. What about something like a recipe that has many ingredients?

class Recipe {
    Long id;
    String name;
    List<Ingredient> ingredients;
}

class Ingredient {
    Long id;
    Recipe recipe;
    String name;
    long amount;
}

Note that the Ingredient has a reference to its owning Recipe. This is sometimes useful, and sometimes not. Hrorm supports object models with or without back-references.

That might be backed by two tables like this.

CREATE TABLE RECIPES (
    ID INTEGER PRIMARY KEY,
    NAME TEXT
);

CREATE TABLE INGREDIENTS (
    ID INTEGER PRIMARY KEY,
    RECIPE_ID INTEGER,
    NAME TEXT,
    AMOUNT INTEGER
);

These models have more significant differences than the cities and states. It's not just that the database description deals with keys rather than the entities themselves, the responsibility for managing the relationship has been reversed. In object model land, the Recipe has a list of Ingredient objects. In the database, the INGREDIENT table contains a column with a foreign key pointing to the RECIPE.

To manage this relationship with Hrorm, we need to make special note in both the recipe and ingredient DaoBuilder objects. We need to start with the ingredient.

DaoBuilder<Ingredient> ingredientDaoBuilder = new DaoBuilder<>("INGREDIENT", Ingredient::new)
        .withPrimaryKey("ID", "INGREDIENT_SEQUENCE", Ingredient::getId, Ingredient::setId)
        .withParentColumn("RECIPE_ID", Ingredient::getRecipe, Ingredient::setRecipe)
        .withStringColumn("NAME", Ingredient::getName, Ingredient::setName)
        .withIntegerColumn("AMOUNT", Ingredient::getAmount, Ingredient::setAmount);

Like the join example, we see that there is a special column defined: a parent column. This alerts Hrorm that the column should be linked to another entity. The RecipeDaoBuilder looks like this.

DaoBuilder<Recipe> recipeDaoBuilder = new DaoBuilder<>("RECIPE", Recipe::new)
        .withPrimaryKey("ID", "RECIPE_SEQUENCE", Recipe::getId, Recipe::setId)
        .withStringColumn("NAME", Recipe::getName, Recipe::setName)
        .withChildren(Recipe::getIngredients, Recipe::setIngredients, ingredientDaoBuilder);

In addition to the column definitions, there is an extra withChildren() call that does not correspond to any column. It requires the accessors of the Recipe model to the Ingredient list as well as the DaoBuilder<Ingredient>. (As above, it really cares about the DaoDescriptor<Ingredient>.)

What does this set-up buy us? Simply this: when we use Hrorm to read or write Recipe objects, we can just about ignore the INGREDIENT table entirely. When we call Dao.insert() on a Dao<Recipe>, Hrorm will make sure to insert all the Ingredient objects in the Recipe automatically. When we use the Dao.update() method, Hrorm will insert, update, and delete Recipe objects without any intervention. Naturally, when you perform a Dao.select(), Hrorm will load all the Ingredient records associated with a Recipe. And when we are done with the recipe, and call Dao.delete(), all the ingredients will be deleted with the recipe.

Associations

A reader might object that neither of the above examples explains how to solve the problem that was originally stated: how to manage actors and movies or any other many-to-many relationship.

One way of categorizing relationship models is by the arity of the relations. One-to-one, one-to-many, many-to-many, etc. Hrorm does not express things in those terms. Hrorm thinks in terms of object models. If one model contains a reference to zero or one instance of another model, that's a sibling relationship, managed by a join. If a model contains a list of other models, that's a parent-child relationship. The arity question does not enter into it. A state has many cities (one-to-many), but the join is still the appropriate relationship.

Thinking only about the Java object model, it might be tempting to define classes for movies and actors as follows

class Movie {
    String title;
    List<Actor> actors;
}

class Actor {
    String name;
    List<Movie> movies;
}

From the perspective of someone using these types, that might be a terrific interface. When you want to now what actors are in a movie, you just call the accessor on the Movie object, and vice versa. But such a model comes with some pitfalls.

  • Everyone who remembers the Kevin Bacon game can think of one problem. Loading up even one movie or actor will result in a giant object graph being loaded, the transitive closure over all the movies and actors related to the one being loaded, maybe the entire data set.
  • That's not ideal. Perhaps we will lazy-load the related movies or actors when they are accessed. Now the object model cannot be made of POJOs, it has to be some kind of record plus connection to the datastore (whether it's an RDBMS, a document store, or just the filesystem).
  • Now there's a new problem. Even after lazy loading, somehow we probably want to have some kind of object pool. After all, as we walk the actor/movie graph, we do not want to have multiple instances of the same movie loaded over and over again, the graph is likely to be big enough, without spending memory on repeated instances.
  • Once that is solved, you have to do some favors for the garbage collector, since nothing can be collected while you have a reference to anything. Perhaps the lists can be weak references?

This is all sounding pretty difficult, which is why Hrorm requires parent-child relationships be in one direction only. Perhaps another approach is in order. We could add a new entity that managed the relationship. In the case of actors and movies this approach is very natural. The entity does not require some artificial name like "ActorMovieConnection", since it already has a name and data we want to associate with it: a role.

class Actor {
    String name;
}

class Movie {
    String title;
}

class Role {
    String characterName;
    Movie movie;
    Actor actor;
}

The backing SQL structures for such a model are straight-forward. Since each Role refers to one Movie and one Actor, we can use Hrorm's sibling relations to manage the Java/SQL divide. Then, when we want to know which actors are in a particular movie (or what movies a particular actor has been in) a simple select on the Dao<Role> will answer our questions.

That is an answer, but it's a bit of a cop-out. What if we have a Foo entity and a Bar entity, but there is nothing that intermediates their many-to-many relationship, and we do not want to pollute the object model with non-existent things? There is no choice in the database, we simply must have an extra table which can record what pairs of Foos and Bars are connected.

If we do not want a role concept in our object model, we can add a SQL table like this.

CREATE TABLE ACTOR_MOVIE_ASSOCIATION (
    ID INTEGER PRIMARY KEY,
    ACTOR_ID INTEGER,
    MOVIE_ID INTEGER
);

Then after the DaoBuilder<Actor> and DaoBuilder<Movie> have been defined, it is possible to create a special object called an AssociationDao. It looks like this.

AssociationDaoBuilder<Actor, Movie> associationDaoBuilder =
            new AssociationDaoBuilder<>(actorDaoBuilder, movieDaoBuilder)
                    .withTableName("ACTOR_MOVIE_ASSOCIATION")
                    .withSequenceName("ACTOR_MOVIE_ASSOCIATION_SEQUENCE")
                    .withPrimaryKeyName("ID")
                    .withLeftColumnName("ACTOR_ID")
                    .withRightColumnName("MOVIE_ID");

All the elements shown above are required. Once the AssociationDaoBuilder has been properly initialized, it just requires a java.sql.Connection to builder a AssociationDao, just as a configured DaoBuilder needs a Connection to build a Dao.

An AssociationDao has a very limited interface. In the current Hrorm release, it has only four methods.

interface AssociationDao<LEFT,RIGHT> {
    List<LEFT> selectLeftAssociates(RIGHT right);
    List<RIGHT> selectRightAssociates(LEFT left);
    Long insertAssociation(LEFT left, RIGHT right);
    void deleteAssociation(LEFT left, RIGHT right);
}

Using this interface, we can walk the tree of relations between movies and actors, without any of the problems outlined above.

Next Steps

If you've read this far and read the other articles about Hrorm, you know most of what there is to know about it. The next step is to use it in your next project. 

Topics:
orm ,java ,database

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}