So, another JPA tutorial. What makes this one different? Well, for one thing, this one comes with a working, downloadable project that works with Eclipse, NetBeans, and IntelliJ IDEA 7. It’s packaged with Hibernate, Toplink, and OpenJPA. And it’s been tested with MySQL, PostgreSQL, MS SQL Server, and Sybase. In other words, it works with 36 different IDE/JPA Provider/Database combinations!
Another thing that makes this tutorial different is the subject matter: Pizza! Who doesn’t love pizza? Except lactose intolerant people. And people who can’t eat gluten. But other than them, who doesn’t love pizza? So we’re going to create a simple database model for a pizza shop’s point-of-sale system.
The
Schema
Unsurprisingly, the starting point for any ORM task is
usually the database schema (there are people who start
with the Objects and work “backward” to the schema, but
I haven’t worked with any of them yet). In our example,
we have a pristine, consistent, completely normalized
schema. In other words, it’s probably nothing like
you’ll ever be lucky enough to see in the real world!
Here’s our simple little ERD:
From
this ERD, we can infer the following: 1.) An order is
comprised of zero or more pizzas. 2.) A pizza is
associated with one size. 3.) A pizza may be associated
with a string of text containing “special instructions.”
4.) A pizza may have zero or more toppings. You probably
also notice that each table has a column called version.
This will be used for an optimistic
locking strategy.
The first question is, “Where should we start?” There’s no right answer for this, but I find that it’s easiest to start working with the entities with the fewest dependencies. For example, you can’t have an order without a pizza, and you can’t have a pizza without a size, so maybe it makes sense to start with the size. But before that, we’ll want an ID interface.
An ID
Interface
First things first. You’ll notice that, in our schema,
every table has an integer ID. It’s often a good idea to
have all of your objects implement the same interface
for accessing the ID, because it makes it easier to
create Generic DAOs (more about that in a future post).
For now, let’s make a really simple interface like
this:
public interface IdObject {
public void setId(Integer id);
public Integer getId();
}Many-to-One
Unidirectional Relationships
Now that we’ve gotten that out of the way, let’s create
the Size class. It’s a simple POJO littered with
annotations, like this:
@Entity
@Table(name="PIZZA_SIZE")
public class Size implements IdObject {
@Id
@Column(name="pizza_size_id")
private Integer id;
@Column(name="pizza_size_description")
private String description;
@Column(name="pizza_size_base_price")
private BigDecimal basePrice;
@Version @Column(name="version")
private Integer version;
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public BigDecimal getBasePrice() { return basePrice; }
public void setBasePrice(BigDecimal basePrice) { this.basePrice = basePrice; }
public Integer getVersion() { return version; }
public void setVersion(Integer version) { this.version = version; }
}Here are some notes on the annotations that were used
in the above class:
@Entity tells the JPA
provider that this is a managed object.
@Table specifies the
table name. The JPA provider will attempt to default the
table name to a sane value based on the class name, but
I like to be explicit. I’m funny that way. Perhaps it’s
OCD.
Or a power-trip.
@Column indicates the
name of the column, and can include other attributes
about the column (you’ll see a few additional attributes
later on). Again, JPA can try to default this to sane
values for you, but I like to be explicit.
@Version indicates
that a particular column is used for indicating when a
row is updated. This column can then be used in an optimistic
locking scheme.
Next, let’s do the Pizza object to show how we map a Pizza to a Size.
One thing I that always tripped me up when I started
out with ORM
tools was the difference between “One-to-Many” and
“Many-to-One.” I never knew, if I call a relationship
many-to-one in my metadata, is this object the one
that there are many of, or is it the other way around?
The answer is that “this” object always comes first.
ManyToOne means that “there are many of
this object to
one of those
objects.” The “Many” side is often the side that has the
foreign key.
In our case, there will be many Pizzas that are the same size. So when we make our Pizza object, we will want to use the @ManyToOne annotation. Here’s what the Pizza object looks like so far. I’ve omitted the getters and setters to save space:
@Entity
@Table(name="PIZZA")
public class Pizza implements IdObject {
@Id
@Column(name="pizza_id")
private Integer id;
@ManyToOne(cascade={CascadeType.ALL})
@JoinColumn(name="pizza_size_id",nullable=false)
private Size size;
@Version @Column(name="version")
private Integer version;
// (Accessor methods omitted)
}Note that, when we made our Size object, we did not include a reference to the Pizza. That was an intentional design decision. In this application, it’s unlikely that we will want to instantiate a Size object, and get a collection containing all of the Pizzas of that size, so we don’t bother with mapping it. This is called unidirectional association.
The @ManyToOne annotation specifies a cascade attribute. There are several different settings for this attribute, which you can read more about here. I tend to cascade the persistent state to all related objects because it reduces the amount of redundant API calls. By default, JPA does not cascade pers istence to related objects. I’ll cover the cascade attribute in future posts, but for now, we’ll go with my personal preference, because I’m writing the article!
The @JoinColumn annotation indicates the column name that defines the linkage between the Pizza and the Size. You’ll also note that we’ve included some additional attributes on our @Column and @JoinColumn annotations. The unique and nullable attributes are particularly useful if you use tools to generate schema DDL from your mappings.
One-to-Many bidirectional
relationships
Both the SpecialInstruction and the Order objects are
examples of One-to-Many bidirectional relationships. In
the case of SpecialInstruction, it is likely that we
will care about which Pizza an instruction is associated
with, and likewise for the Order. A bidirectional
one-to-many relationship implies that one object has a
collection of
other o bjects. For example, an Order has a collection
of Pizzas. First, lets add an order attribute to our
Pizza object:
@Entity
@Table(name="PIZZA")
public class Pizza implements IdObject {
.
.
@ManyToOne(cascade={CascadeType.ALL})
@JoinColumn(name="pizza_order_id",nullable=false)
private Order order;
public Order getOrder() { return order; }
public void setOrder(Order order) { this.order = order; }
.
.
}Next, let’s create an Order object to contain our collection of Pizzas, like this:
@Entity
@Table(name="PIZZA_ORDER")
public class Order implements IdObject {
@Id
@Column(name="pizza_order_id")
private Integer id;
@OneToMany(cascade={CascadeType.ALL},mappedBy="order")
private Set pizzas = new HashSet();
@Version @Column(name="version")
private Integer version;
// (version and id accessors omitted)
public Set getPizzas() { return pizzas; }
public void setPizzas(Set pizzas) { this.pizzas = pizzas; }
public void addPizza(Pizza pizza) { pizza.setOrder(this); this.pizzas.add(pizza); }
}There are a couple of things you worth noting about
this mapping:
1.) The @OneToMany
annotation uses the mappedBy attribute to
indicate which member of the related object defines the
linkage between the two tables. In this case, we are
saying that the Pizza object contains a member named
order, which defines the linkage between the two
objects.
2.) I’ve created a utility method called addPizza. This
simplifies setting both sides of the bidirectional
relationship by setting the Order object on the Pizza
and adding the
Pizza to the Order’s collection. Users of this class
will only need to make one method call to do both.
Many-to-Many relationships via a Join
Table
The last thing we’ll cover is how to map Join Tables. In
our ERD, you can see that Toppings are modeled in the
database with a TOPPING table that contains all of the
valid toppings, a PIZZA table that contains all of the
valid Pizzas, and a PIZZA_TOPPING table in the middle
that maps all of the valid Pizzas to all of the valid
Toppings. You could create an
object called PizzaTopping that corresponds to the
PIZZA_TOPPING table. Then you could have a One-to-Many
relationship from the Pizza to the PizzaTopping, and a
One-to-One from each PizzaTopping to a Topping. That
would be very cumbersome to work with! Fortunately,
there’s a better way.
Logically, a Pizza has a collection of Toppings. In our Java code, we really shouldn’t care about the fact that there is a PIZZA_TOPPING join table in the middle. First, let’s create a simple Topping class:
@Entity
@Table(name="TOPPING")
public class Topping implements IdObject {
@Id
@Column(name="topping_id")
private Integer id;
@Column(name="topping_description")
private String description;
@Column(name="topping_price")
private BigDecimal price;
@Version @Column(name="version")
private Integer version;
// (Accessors omitted)
}This is how the association would be mapped in the Pizza class:
@Entity
@Table(name="PIZZA")
public class Pizza implements IdObject {
.
.
@ManyToMany(cascade={CascadeType.ALL})
@JoinTable(name="PIZZA_TOPPING",
joinColumns=@JoinColumn(name="pizza_id"),
inverseJoinColumns=@JoinColumn(name="topping_id"))
private Set toppings = new HashSet();
public Set getToppings() { return toppings; }
public void setToppings(Set toppings) { this.toppings = toppings; }
public void addTopping(Topping topping) { this.toppings.add(topping); }
.
.
}The @JoinTable annotation
defines three key attributes:
name: Identifies
the name of the join table.
joinColumns:
This attribute identifies the column name in the join
table that points to this object.
inverseJoinColumns:
This attribute defines the column name in the join table
that points to the
other objects.
From your Java code, the semantics for dealing with toppings on a pizza are just like any other set, much like you’d work with a One-to-many object.
Conclusion
Since the introduction of annotations, object/relational
mapping is one of the easiest aspects of working with
JPA over other frameworks. You can see this whole
project in action on your IDE of choice by downloading
it here
(or from here
if that doesn’t work for some reason). It should be a
pretty reasonable starting point if you want a reference
project to start playing with JPA. I hope to revisit the
Pizza Shop project to cover other JPA topics in future
posts.