Interactions with hrorm have two major points: building Dao
objects and then using them.
Dao
building is accomplished with the aptly named DaoBuilder
class.
DaoBuilder
objects are part of the one-time (static, singleton) initiation of
your application. There is little point in having more than one builder of any entity type.
Of course, some care must be taken: by their nature, DaoBuilder
objects are
mutable, so if you directly expose them to the rest of your application during start up,
it's possible that you can do something stupid.
Dao
objects themselves are what perform the actual tasks of persisting and
instantiating entity objects. To make a Dao
requires a Connection
object. Since a Dao
keeps a stateful Connection
to the underlying
data store, it is dangerous to share instances across threads. Generally, the idea is to
instantiate a Dao
when you need it, and then allow it to be garbage collected.
It is up the application itself to deal with reaping the Connection
, with some exceptions
noted below.
Also take a look at the Quick Start and Javadocs.
This page contains more nuts and bolts about what and how hrorm does what it does. For more on the why, return to the hrorm home page.
The easiest case for any ORM tool is persisting a single object backed by a single table. Let's work on persisting a model for a person that includes the following elements:
To model the person entity, we write a Java Person object.
class Person { Long id; String name; long weight; BigDecimal height; LocalDateTime birthday; boolean isHighSchoolGraduate; HairColor hairColor; }
A few small points.
HairColor.Black
, HairColor.Brown
, etc.In the database we will create two structures for persisting this class: a table to store the data and a sequence to issue the IDs.
CREATE SEQUENCE PERSON_SEQUENCE; CREATE TABLE PERSON_TABLE ( ID INTEGER PRIMARY KEY, NAME TEXT, WEIGHT INTEGER, HEIGHT DECIMAL, BIRTHDAY TIMESTAMP, IS_HIGH_SCHOOL_GRADUATE TEXT, HAIR_COLOR TEXT );
Note the somewhat different types than in the Java code. In particular, the boolean and enumeration values have become text fields. SQL has a more limited palate for the expression of types than Java.
To translate between the database reprentation and the Java representation, we plan to use
a Dao
object. We could construct that directly, but hrorm provides a DaoBuilder
class that makes things much easier. In hrorm, both Dao
objects and their builders
are parameterized on the type of thing they persist. We start off by simply calling the
DaoBuilder
constructor.
DaoBuilder<Person> daoBuilder = new DaoBuilder<>("PERSON_TABLE", Person::new);
The constructor takes two arguments: the name of the table and a no-argument method for creating a new instance of the parameterized type.
Next, we need to define the primary key for this entity.
daoBuilder.withPrimaryKey("ID","PERSON_SEQUENCE", Person::getId, Person::setId);
The primary key is defined with four elements:
Long
) from the Person
object.With that covered, we can being to teach the DaoBuilder
about the individual data
elements. First, we will teach it about the name field.
daoBuilder.withStringColumn("NAME", Person::getName, Person::setName);
This explains that that table has a column named "NAME" and that the value in the table can
be populated from calling getName()
on a Person
, and that value
can be set by calling setName()
. There are other methods on the DaoBuilder
for other Java types.
For the integer weight value (which should actually be a Long
or long
,
not an int
or short
).
daoBuilder.withIntegerColumn("WEIGHT", Person::getWeight, Person::setWeight);
For fractional, decimal, or floating point values, hrorm supports the java.math.BigDecimal
type.
daoBuilder.withBigDecimalColumn("HEIGHT", Person::getHeight, Person::setHeight);
For dates and times, hrorm supports the java.time.LocalDateTime
type.
daoBuilder.withLocalDateTimeColumn("BIRTHDAY", Person::getBirthday, Person::setBirthday);
And similarly for boolean values. Note that the table should declare a text or string value. ANSI SQL does not support a boolean column type. Hrorm will convert between Java's boolean enumeration of true/false with the strings "T" and "F" in the database.
daoBuilder.withBooleanColumn("IS_HIGH_SCHOOL_GRADUATE", Person::isHighSchoolGraduate, Person::setHighSchoolGraduate);
For the enumerated HairColor
type, hrorm needs a bit more help, via an implementation
of its Converter
interface. We need a simple class that looks like this:
class HairColorConverter implements Converter<HairColor, String> { @Override public String from(HairColor item) { return item.getColorName(); } @Override public HairColor to(String s) { return HairColor.forColorName(s); } }
Once the Converter
exists, we can teach the DaoBuilder
about it
and the hair color field.
daoBuilder.withConvertingStringColumn("HAIR_COLOR", Person::getHairColor, Person::setHairColor, new HairColorConverter());
Notice that in addition to the usual fields for column name, getter, and setter, we additionally must specify the conversion mechanism.
That completes the DaoBuilder
. Now we can actually build a Dao<Person>
object, assuming we have a java.sql.Connection
.
But before that, we should note that the DaoBuilder
supports a fluent interface,
so we could write all of the above as:
DaoBuilder<Person> daoBuilder = new DaoBuilder<>("PERSON_TABLE", Person::new) .withPrimaryKey("ID","PERSON_SEQUENCE", Person::getId, Person::setId) .withStringColumn("NAME", Person::getName, Person::setName) .withIntegerColumn("WEIGHT", Person::getWeight, Person::setWeight) .withBigDecimalColumn("HEIGHT", Person::getHeight, Person::setHeight) .withLocalDateTimeColumn("BIRTHDAY", Person::getBirthday, Person::setBirthday) .withBooleanColumn("IS_HIGH_SCHOOL_GRADUATE", Person::isHighSchoolGraduate, Person::setHighSchoolGraduate) .withConvertingStringColumn("HAIR_COLOR", Person::getHairColor, Person::setHairColor, new HairColorConverter());
In just 8 lines of code, we have taught hrorm everything it needs to know to CRUD Person
objects.
When one entity object contains a reference to another entity object, hrorm calls that a sibling or join relationship.
Consider a model of cities and states, where each city contains a reference to a state.
class State { Long id; String name; } class City { Long id; String name; State state; }
This could be backed by this schema.
CREATE TABLE STATE ( ID INTEGER PRIMARY KEY, NAME TEXT, ); CREATE TABLE CITY ( ID INTEGER PRIMARY KEY, NAME TEXT, STATE_ID INTEGER ); CREATE SEQUENCE STATE_SEQUENCE; CREATE SEQUENCE CITY_SEQUENCE;
Creating the State
DaoBuilder
is trivial.
DaoBuilder<State> stateDaoBuilder = new DaoBuilder<>("STATE", State::new) .withPrimaryKey("ID", "STATE_SEQUENCE", State::getId, State::setId) .withStringColumn("NAME", State::getName, State::setName);
There is one new trick to creating the City
DaoBuilder
: using the
DaoBuilder.joinColumn()
method which will refer to the stateDaoBuilder
we just defined.
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);
The withJoinColumn
method accepts an extra parameter: a DaoDescriptor
.
Both DaoBuilder
and the Dao
class implement this interface. Generally,
it's much more convenient to create all the builder objects together.
Sibling or join relationships in hrorm are one-way. One object declares that it has a reference to another. Trying to make a circular relationship will lead to errors.
When hrorm instantiates objects like City
from the database, it automatically
instantiates the appropriate sibling State
objects and sets the field in the
City
object.
Of course, you could just treat these as two one-table Dao
objects, and then
right some code to glue things together. In addition to being inconvenient, this will likely
have poorer performance, since hrorm will issue a SQL left join to load the City
and State
objects with one query.
Objects can have several join columns, and those objects can have their own join columns.
Hrorm will attempt to transitively load the entire object graph when a select()
method is called
on the Dao
. There is a limit to how many joins hrorm can perform. Additionally, there
is a limit to how many joins a database engine will allow. Consider this when designing Dao
objects.
Also remember, sibling relationships are for reading and populating objects,
not for making saves or updates. If a sibling object is mutated, it must be saved itself.
When one entity contains a collection of other entities, hrorm calls that a parent child relation.
Here is a simple model for tracking inventories of stocks of things through time. At each instant that we measure, we want to know what quantity of each product we have.
public class Inventory { Long id; LocalDateTime date; List<Stock> stocks; } public class Stock { Long id; Inventory inventory; String productName; BigDecimal amount; }
The Inventory
class represents a snapshot in time of what was available in inventory,
modeled as a List
of Stock
items, each of which contains a product name
and a decimal quantity of how much of that thing is available. Notice that the Stock
model
includes a reference to the inventory ID, but not the inventory object itself.
To model this in the database, we make each item in the STOCK
table point back to
an INVENTORY
record, as follows.
CREATE TABLE INVENTORY ( ID INTEGER PRIMARY KEY, DATE TIMESTAMP ); CREATE TABLE STOCK ( ID INTEGER PRIMARY KEY, INVENTORY_ID INTEGER, PRODUCT_NAME TEXT, AMOUNT DECIMAL ); CREATE SEQUENCE INVENTORY_SEQUENCE; CREATE SEQUENCE STOCK_SEQUENCE;
To model this in hrorm, we need to teach it about the parent-child relationship between the two entities
using the DaoBuilder.withParentColumn()
and DaoBuilder.withChildren()
methods.
First we make a Dao for the Stock
entity.
DaoBuilder<Stock> stockDaoBuilder = new DaoBuilder<>("STOCK", Stock::new) .withPrimaryKey("ID","STOCK_SEQUENCE", Stock::getId, Stock::setId) .withParentColumn("INVENTORY_ID", Stock::getInventory, Stock::setInventory) .withStringColumn("PRODUCT_NAME", Stock::getProductName, Stock::setProductName) .withBigDecimalColumn("AMOUNT", Stock::getAmount, Stock::setAmount);
The column INVENTORY_ID
is marked not as an integer column,
but with the special withParentColumn
method.
An entity can have only one parent. In the Inventory
DaoBuilder
we use the withChildren
method
to complete the relationship definition..
DaoBuilder<Inventory> inventoryDaoBuilder = new DaoBuilder<>("INVENTORY", Inventory::new) .withPrimaryKey("ID", "INVENTORY_SEQUENCE", Inventory::getId, Inventory::setId) .withLocalDateTimeColumn("DATE", Inventory::getDate, Inventory::setDate) .withChildren("INVENTORY_ID", Inventory::getStocks, Inventory::setStocks);
Notice that the withChildren()
call references the name of the column on
the STOCK
table.
When we create a Dao
in this fashion we create a category of entity, the child,
that is wholly dependent upon another, the parent. Whenever we insert, update, delete, or
select the parent entity, the changes we make flow through the children and transitively
to their children.
Be careful, if you do not want the children to be deleted, this is not the relationship
you want to construct. In particular, remember that issuing an update
will result not just in a SQL UPDATE
in the database, but possibly
a whole series of INSERT
, UPDATE
, and DELETE
queries being run.
Hrorm always understands child objects to be members of type List
.
No other collection type is supported.
Documentation on constraints to come.
To create a Dao
from a DaoBuilder
, just pass it a java.sql.Connection
:
// Assume the existence of some ConnectionPool Connection connection = ConnectionPool.connect(); Dao<Person> dao = daoBuilder.buildDao(connection);
To create a new record in the database, we create a new instance of the class and pass it
to Dao.insert()
.
Person person = new Person(); // set values for the fields we want person.setName("Thomas Bartholomew Atkinson Wilberforce"); person.setHighSchoolGraduate(true); person.setWeight(100L); long id = dao.insert(person); connection.commit();
After that code runs, the record will be stored in the database. Hrorm will have pulled a new sequence value and set it on the object. The following assertions will be true.
Assert.assertNotNull(person.getId()); Assert.assertTrue(id == person.getId());
Hrorm will automatically insert child records of the instance being saved, if any.
If the record has sibling entities, references to those will be persisted. But be careful, those sibling references must be persisted first. Sibling inserts and updates do not cascade.
Hrorm provides a few methods for reading data out of the database and instantiating entity objects.
All of the selection mechanisms below will fully read and populate the entire relevant object graph including all children and siblings and all their transitive references.
You can read an item from the database if you know its primary key.
Person person = dao.select(432L);
If you want to read several IDs at once, you can.
List<Person> personList = dao.selectMany(Arrays.asList(432L,21L,7659L));
If you want all the records (presumably for a smallish table) just do
List<Person> personList = dao.selectAll();
Most of the time, you do not know up front what ID or IDs you are intersted in, so hrorm allows you to select by columns. To do this, we make an instance of the entity we are searching for and populate it with the values we want to match. Suppose we want to find all the people who are high school graduates that weigh 100 kilograms. Here's what we can do.
Person personTemplate = new Person(); personTemplate.setHighSchoolGraduate(true); personTemplate.setWeight(100L); List<Person> people = personDao.selectManyByColumns(personTemplate, "IS_HIGH_SCHOOL_GRADUATE", "WEIGHT");
Notice that hrorm wants the names of the database columns, not the fields on the object.
If we know that a particular query will only return 0 or 1 results (for instance, if there is a uniqueness constraint on the name field), hrorm provides a convenience method for that.
Person personTemplate = new Person(); personTemplate.setName("Rumplestiltskin"); Person person = personDao.selectByColumns(personTemplate, "NAME");
If you use this method and hrorm finds more than one record, it will raise an exception.
At the moment, hrorm only supports exact matches, not any LIKE
syntax.
After making changes to the state of the object, we can call
dao.update(person); connection.commit();
This will issue an update in the database based on the primary key (id
) field.
Updates will automatically propagate to children, but not to siblings.
When we are done with a person, we can issue
dao.delete(person); connection.commit();
To remove the record from the database, using the primary key, as with an update.
Deletes will automatically propagate to children, but not to siblings.