We will modify our application again and introduce some more object-relational mapping annotations. Specifically we will cover the @Embedded, @Embeddable, @Enumerated, and @MapKey annotations.
First we decide to embed the Referee entity within the Customer entity. An embedded Referee entity shares the identity of the owning Customer entity. A Referee entity cannot exist on its own without a corresponding Customer entity. Such a Referee entity cannot be independently persisted, it is persisted by default whenever the owning Customer entity is persisted. However, we still map a Referee object onto a relational database in the usual manner.
We also decide to add an enumerated type, gender, as a Customer attribute.
To indicate an entity is embedded EJB 3 provides two annotations: @Embedded and @Embeddable. @Embedded is used within the owning entity and @Embeddable within the embedded entity. Below is the modified version of the Customer entity:
@Entity
public class Customer implements Serializable {
private int id;
private String firstName;
private String lastName;
private Referee referee;
private Collection<Address> addresses;
private Map<String, Account> accounts =
new HashMap<String, Account>();
public Customer() {}
@Id
@Column(name="CUSTOMER_ID")
public int getId() { return id; }
public void setId(int id) { this.id = id; }
@Column(name="FIRST_NAME", length=30)
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) {
this.firstName = firstName; }
@Column(name="LAST_NAME", length=30)
public String getLastName() { return lastName; }
public void setLastName(String lastName) {
this.lastName = lastName; }
@OneToOne
@Embedded
public Referee getReferee() { return referee; }
public void setReferee(Referee referee) {
this.referee = referee; }
@OneToMany(mappedBy="customer", fetch=EAGER)
@MapKey()
public Map<String, Account> getAccounts() {
return accounts; }
public void setAccounts(Map<String, Account> accounts) {
this.accounts = accounts; }
@ManyToMany(fetch=EAGER)
@JoinTable(
name="CUSTOMER_ADDRESS",
joinColumns=@JoinColumn(
name="CUST_ID",
referencedColumnName="CUSTOMER_ID"),
inverseJoinColumns=@JoinColumn(
name="ADD_ID",
referencedColumnName="ADDRESS_ID")
)
public Collection<Address> getAddresses() {
return addresses; }
public void setAddresses(Collection<Address> addresses) {
this.addresses = addresses; }
private Gender gender; @Column(name="GENDER", length=20) @Enumerated(STRING)
public Gender getGender() { return gender; }
public void setGender(Gender gender) {
this.gender = gender;
}
public String toString() {
return "[Customer Id =" + id + ",first name=" +
firstName + ",last name=" + lastName + ",
referee=" + referee + ",addresses=" +
addresses + ",accounts=" + accounts +
",gender=" + gender + "]";
}
}
As you can see in the section above, we have added the @Embedded annotation on the getRefeee() method to indicate that the Referee entity is embedded.
Note that we have added a new field, gender, to the Customer entity. The gender field will be persisted as an enumerated type with enumeration constants of MALE and FEMALE. This is implemented in EJB 3 using the @Enumerated annotation. This can be a field-based or property-based annotation. In our example we prefix the getGender() method with @Enumerated :
Note that we have specified STRING in the @Enumerated annotation. This specifies that the field will be mapped as a string. If we specify ORDINAL, which is the default, then the field will be mapped as an integer.
We now need to create the enumeration class itself, Gender.java :
package ejb30.entity;
public enum Gender { MALE, FEMALE }
Because we specified a value of STRING in the @Enumerated annotation, one of the string values MALE or FEMALE will be stored in the database. If we had specified ORDINAL, then 0 would be stored in the database if the enumeration value is MALE and 1 would be stored if the enumeration value is FEMALE.
We will make one more modification to the Customer entity. We want to store Account objects associated with a Customer as a Map. We define an accounts object to be a Map in the usual manner:
private Map<String, Account> accounts =
new HashMap<String, Account>();
This defines a Map, accounts, where the key is a String type and the value is an Account object.
We now need to use the @MapKey annotation and modify the associated getter and setter as follows:
@OneToMany(mappedBy="customer", fetch=EAGER)
@MapKey()
public Map<String, Account> getAccounts() {return accounts;}
public void setAccounts(Map<String, Account> accounts) {
this.accounts = accounts;
}
The @MapKey defaults to storing the primary key of the associated entity as the map key. In this case the id property of Account is stored. If we want to specify a property other than the primary key we need to use the name attribute of @MapKey. For example, if in our banking example we also had a rollNumber property which is unique for an Account object, we would specify:
@MapKey(name=rollNumber)
Note that we also modified the return type of getAccounts() and the argument type of setAccounts().
Referee Class
We now turn to the Referee class:
//@Entity
@Embeddable
public class Referee implements Serializable {
private int id;
private String name;
private String comments;
// getters and setters
}
We have added the @Embeddable annotation to indicate that all Referee fields are persisted only when the Customer entity is persisted. Because Referee cannot be persisted on its own it is no longer an entity so we do not use the @Entity annotation. There are no other changes to the Referee class. We cannot use any mapping annotations apart from @Basic, @Column, @Lob, @Temporal, and @Enumerated within an embeddable class.
BankServiceBean
We need to make a few changes to BankServiceBean as shown in the following code fragment:
@Stateless
public class BankServiceBean implements BankService {
@PersistenceContext(unitName="BankService")
private EntityManager em;
public void createCustomers() {
Referee r1 = new Referee();
r1.setId(1);
r1.setName("SIR JOHN DEED");
r1.setComments("JUDGE");
// em.persist(r1);
Customer c1 = new Customer();
c1.setId(1);
c1.setFirstName("SIMON");
c1.setLastName("KING");
c1.setReferee(r1);
c1.setGender(Gender.MALE);
Account a1 = new Account();
a1.setId(1);
a1.setBalance(430.5);
a1.setAccountType("C");
a1.setCustomer(c1);
Map<String, Account> accounts1 = new HashMap<String, Account>(); accounts1.put("1", a1);
c1.setAccounts(accounts1);
em.persist(a1);
...
}
public Map<String, Account> findAccountForCustomer(
int custId) {
Customer customer = (Customer) em.find(Customer.class,
custId);
Map<String, Account> accounts =
customer.getAccounts();
return accounts;
}
// other finder methods
}
First, as Referee is now an embedded entity, statements which persist instances of the Referee entity need to be deleted:
// em.persist(r1);
Next, as each Customer object now has an associated Gender enumerated type, we need to invoke the setGender() method passing either a MALE or FEMALE argument:
c1.setGender(Gender.MALE);
Finally, we have added the findAccountForCustomer() method which returns a Map of accounts for a given customer.
Composite Primary Keys
It is good practice to have a single column as a primary key. It is also good practice for the primary key value to be obtained from a sequence and not contain any data. A primary key containing data is very hard to update. A primary key obtained from a sequence is known as a surrogate key. The main advantage of a surrogate key is that we never need to update the key. However, there may be occasions where we need to map an entity to a legacy relational database which has a composite primary key for the corresponding table.
Suppose our Customer entity has a composite primary key of FIRST_NAME, LAST_NAME in the corresponding CUSTOMER table. First we must define a primary key class, CustomerPK.java, as follows:
public class CustomerPK implements Serializable {
private String firstName = "";
private String lastName = "";
public CustomerPK() {}
public CustomerPK(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// getter and setter methods.
// equals and hashcode method.
}
A primary key class must be public, serializable, and have a public no-arg constructor. The class must also define equals and hashcode methods.
Now let's look at the Customer entity:
@Entity
@IdClass(CustomerPK.class)
public class Customer implements Serializable {
...
@Id
@Column(name="FIRST_NAME", length=30)
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
@Id
@Column(name="LAST_NAME", length=30)
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
...
}
Note that we use the class level @IdClass annotation to specify the primary key class associated with the Customer entity. We use the property-based @Id annotation for each of the getter methods corresponding to the fields, firstName and lastName, making up the primary key.
Now look at the Account entity:
@Entity
public class Account implements Serializable {
...
@ManyToOne
@JoinColumns({
@JoinColumn(name="CUSTOMER_FNAME",
referencedColumnName="FIRST_NAME"),
@JoinColumn(name="CUSTOMER_LNAME",
referencedColumnName="LAST_NAME")
})
public Customer getCustomer() {
return customer;
}
...
}
When defining the many-to-one relationship between Account and Customer, we need to modify the join column definitions. We now have more than one join column; actually two columns which comprise the composite key. Consequently we use the @JoinColumns annotation wrapped around the individual @JoinColumn annotations.