Hi,
Below is the basic skeleton of part of my business model (I only kept the primary key attributes/relationships and removed all the other trivial stuff):
Code:
@MappedSuperclass
public abstract class Person implements Serializable {
private Long id;
@Id
@GeneratedValue(strategy=GenerationType.TABLE, generator="tableGenerator")
public Long getId() {
return this.id;
}
private void setId(Long id) {
this.id = id;
}
}
@Entity
@Inheritance
@DiscriminatorColumn(name = "DISCRIMINATOR")
public abstract class Administrator extends Person {
private AdministratorAppUser appUser;
@OneToOne(mappedBy = "administrator", cascade = ALL)
public AdministratorAppUser getAppUser() {
return appUser;
}
public void setAppUser(AdministratorAppUser appUser) {
appUser.setAdministrator(this);
this.appUser = appUser;
}
}
@Entity
public class SystemAdministrator extends Administrator {
}
@Entity
public class CustomerSupport extends Administrator {
}
@Entity
public abstract class Customer extends Person {
private CustomerAppUser appUser;
@OneToOne(mappedBy = "customer", cascade = ALL)
public CustomerAppUser getAppUser() {
return appUser;
}
public void setAppUser(CustomerAppUser appUser) {
appUser.setCustomer(this);
this.appUser = appUser;
}
}
@Entity
@Inheritance
@DiscriminatorColumn(name = "DISCRIMINATOR")
public abstract class AppUser implements Serializable {
private Long id;
@Id
@GeneratedValue(strategy=GenerationType.TABLE, generator="tableGenerator")
public Long getId() {
return this.id;
}
private void setId(Long id) {
this.id = id;
}
@Transient
public abstract Person getPerson();
}
@Entity
public class AdministratorAppUser extends AppUser {
private Administrator administrator;
@OneToOne(optional = false)
@JoinColumn(name = "ADMINISTRATOR_ID")
public Administrator getAdministrator() {
return administrator;
}
public void setAdministrator(Administrator administrator) {
this.administrator = administrator;
}
@Override
@Transient
public Person getPerson() {
return getAdministrator();
}
}
@Entity
public class CustomerAppUser extends AppUser {
private Customer customer;
@OneToOne(optional = false)
@JoinColumn(name = "CUSTOMER_ID")
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
@Override
@Transient
public Person getPerson() {
return getCustomer();
}
}
And here's the physical model:
table ADMINISTRATOR(contains entities SystemAdministrator and CustomerSupport)
table CUSTOMER (contains entity Customer)
table APP_USER (contains entities AdministratorAppUser and CustomerAppUser)
In other words, I have customers (Customer) and administrators (Administrator) that can login to a site. Administrators are divided in types: system administrators (SystemAdministrator) and customer support people (CustomerSupport). All of these entities share a mapped superclass (Person) (for a reason that I don't want to get into right now, this cannot be an Entity). The credentials of each of these people are stored in a separate table (AppUser).
If I kept the relationships between the people and their assocatied user unidirectionnal, the login process would have to do many EQL queries to find the person whose actually logging base solely on his credentials because querying the AppUser entity directly wouldn't allow my to go back to its "owner". So I figured I would make the relationship bi-directionnal. All the people who own a user do not share a parent entity (Person being a mapped superclass, it cannot be the target of a relationship), so I cannot have a polymorphic relation from AppUser back to any of its owner. I figured I would sub-class the AppUser entity for each of the people who own it (AdministratorAppUser, CustomerAppUser) and create the bi-directionnal relation in each subclass back to the proper owner (AdministratorAppUser => Administrato and CustomerAppUser => Customer). Now, when I query the AppUser entity, it instantiates the proper sub-class (AdministratorAppUser or CustomerAppUser), on which I can invoke the abstract method "getPerson", which is overrided in each of the sub-class to return the true owner using a typed relation in each one (AdministratorAppUser returns getAdministrator() and CustomerAppUser returns getCustomer()). This all works great but I can't get it to optimize the number of queries.
Let's say I do this:
Code:
AppUser user = (AppUser)entityManager.createQuery("select u from AppUser as u where u.username = 'blabla' and u.password = 'potato'").getSingleResult();
Person owner = user.getPerson();
Here are the 2 SQL queries generated (again I removed all the trivial columns):
Code:
/* load org.mcgill.moxxi.entity.user.AppUser */ select
appuser0_.ID as ID1_3_,
appuser0_.DISCRIMINATOR as DISCRIMI1_1_3_,
administra1_.ID as ID0_0_,
administra1_.DISCRIMINATOR as DISCRIMI1_0_0_,
customer2_.ID as ID8_1_,
customer2_.DISCRIMINATOR as DISCRIMI1_8_1_
from
APP_USER appuser0_
left outer join
ADMINISTRATOR administra1_
on appuser0_.ADMINISTRATOR_ID=administra1_.ID
left outer join
CUSTOMER customer2_
on appuser0_.CUSTOMER_ID=customer2_.ID
where
appuser0_.ID=?|#]
Code:
/* load org.mcgill.moxxi.entity.administrator.AdministratorAppUser */ select
administra0_.ID as ID1_1_,
administra1_.DISCRIMINATOR as DISCRIMI1_0_0_
from
APP_USER administra0_
left outer join
ADMINISTRATOR administra1_
on administra0_.ADMINISTRATOR_ID=administra1_.ID
where
administra0_.ADMINISTRATOR_ID=?
and administra0_.DISCRIMINATOR='AdministratorAppUser'|#]
You see that in the first query, it saw the the AppUser had 2 sub-classes that each defined a OneToOne with fetch = EAGER (the default for @OneToOne) so it generated 2 left outer joins, one for each of them (CustomerAppUser.customer pointing to the CUSTOMER table and AdministratorAppUser.administrator pointing to the ADMINISTRATOR table). What I cannot explain is the 2nd query. When debugging, I found that the 1st query hydrated 2 entities (AdministratorAppUser and Administrator). Since my test was with an admin user, this makes sense. What I dont' understand is that when it initializes the Administrator entity, it tries to reload the invert part of the relation that points back to the AdministratorAppUser (the Administrator.appUser property) but using the primary key of the Administrator itself. That one, I can't figure out. Would anyone know why?
I also have a problem with auto generation of the database schema. For my AppUser class hierarchy, I have a single table inheritance strategy (the default value of @Inheritance). In the CustomerAppUser and AdministratorAppUser classes, I would like to make the relations that point back to the proper owner (CustomerAppUser.customer and AdministratorAppUser.administrator) optional = false because from their point of view, it doesn't make sense for those values to be null. The thing is if I do, both foreign key columns are generated not null. Since they are stored in the same table and are mutually exclusive, this is a problem. How come the DDL tool didn't realize that even though each relation is defined as mandatory, since they are defined in a specific sub-class of a single table inheritance strategy and not in the base class, it should generate a nullable column because they are mutually exclusive?
Sorry of this is a little bit hard to follow. I can provide more details if necessary. Thank you very much for any help you can provide.
Hibernate Core version: 3.2.5
Hibernate Entity Manager version: 3.3.1
Hibernate Annotations version: 3.3.0