Alright, let's bring this back specifically to Hibernate, and make a specific Hibernate extension proposal. If no one else thinks this is a good idea, but we wind up wanting it badly enough, perhaps we'll implement it and submit it to JIRA! (What licensing issues would that bring up, though? Sigh....)
First off: we are using hbm2java to generate our base persistent Java classes. These hbm2java-generated Java classes really *are* DTOs -- they contain no business logic, just the persistent data of our domain model. (We assume that it's OK to expose our basic domain model entities and relationships as DTOs, since we don't see the need for another whole layer of code.)
We want to extend these Hibernate-generated DTO-like classes with business logic. We'll be using <meta attribute="generated-class"> for this (as suggested in other threads I started ;-).
Let's look at a specific scenario: we have a Campaign object which has a parent Client object. We want to put interesting business methods (for example, "delete" which will remove the parent's link) on the Campaign object. We also want to pass Campaign instances to the presentation tier (which has NO ability to reach or use the persistence layer). But we don't want the business methods to be callable by the presentation tier.
In Hibernate 2.1.1, we can do this:
Code:
<hibernate-mapping>
<class name="com.nimblefish.core.data.Campaign">
<meta attribute="generated-class">com.nimblefish.core.data.CampaignDTO</meta>
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<property name="name" type="string"/>
<many-to-one name="client" column="client_id" not-null="true"
class="com.nimblefish.core.data.Client"/>
<!-- etc., etc. -->
</class>
</hibernate-mapping>
This generates the class:
Code:
/** @author Hibernate CodeGenerator */
public class CampaignDTO implements Serializable {
/** identifier field */
private Long id;
/** nullable persistent field */
private String name;
/** persistent field */
private com.nimblefish.core.data.Client client;
/** full constructor */
public CampaignDTO(String name, com.nimblefish.core.data.Client client) {
this.name = name;
this.client = client;
}
/** default constructor */
public CampaignDTO() {
}
/** minimal constructor */
public CampaignDTO(com.nimblefish.core.data.Client client) {
this.client = client;
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public com.nimblefish.core.data.Client getClient() {
return this.client;
}
public void setClient(com.nimblefish.core.data.Client client) {
this.client = client;
}
}
And we then subclass it:
Code:
public class Campaign extends CampaignDTO {
public void delete () {
// disconnect the parent-child association
this.getClient().getCampaigns().remove(this);
Persistence.session().delete(this);
}
public float calculateCampaignValue () {
// do some major business logic....
}
}
Client is implemented exactly the same way - the mapping specifies the class name "Client", the generated-class is "ClientDTO", and we subclass it to add the business logic to the actual "Client" class.
Now, what we really want to be able to pass back to the presentation tier is the CampaignDTO, not the Campaign.
The problem is that the CampaignDTO has a binding to the Client class, not the ClientDTO class. In other words, CampaignDTO has a getClient method, which exposes the Client business object, rather than a getClientDTO method, which would just expose the transfer object.
In some sense, what we want is to have Hibernate generate is something more like this:
Code:
/** @author Hibernate CodeGenerator */
public class CampaignDTO implements Serializable {
/** identifier field */
private Long id;
/** nullable persistent field */
private String name;
/** persistent field */
private com.nimblefish.core.data.ClientDTO client;
/** full constructor */
public CampaignDTO(String name, com.nimblefish.core.data.ClientDTO client) {
this.name = name;
this.client = client;
}
/** default constructor */
public CampaignDTO() {
}
/** minimal constructor */
public CampaignDTO(com.nimblefish.core.data.ClientDTO client) {
this.client = client;
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public com.nimblefish.core.data.ClientDTO getClientDTO() {
return this.client;
}
public void setClientDTO(com.nimblefish.core.data.ClientDTO client) {
this.client = client;
}
}
Then we could extend our child object as follows:
Code:
public class Campaign extends CampaignDTO {
public Client getClient () { return (Client)getClientDTO(); }
public void setClient (Client client) { setClientDTO(client); }
public void delete () {
// disconnect the parent-child association
this.getClient().getCampaigns().remove(this);
Persistence.session().delete(this);
}
public float calculateCampaignValue () {
// do some major business logic....
}
}
This basically leaves the Campaign interface completely unchanged. However, we can then write a generic domain-objects-to-DTO-objects mapper that walks over some domain object, constructing a set of DTO objects (using totally generic reflection), handling however much (or little) transitive loading as is appropriate in the given use case. (For example, we could take a Campaign object which has a Client object, and create a CampaignDTO object which has a ClientDTO object. Exactly the same object graph, only with just the DTO superclass instances.)
Then all we ever need to expose to the client is the DTO object classes, which have no reference whatsoever to the business object classes.
I guess what I'm really asking for is some new meta tag on properties, something like this:
Code:
<hibernate-mapping>
<class name="com.nimblefish.core.data.Campaign">
<meta attribute="generated-class">com.nimblefish.core.data.CampaignDTO</meta>
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<property name="name" type="string"/>
<many-to-one name="client" column="client_id" not-null="true"
class="com.nimblefish.core.data.Client">
<meta attribute="declared-type" class="com.nimblefish.core.data.ClientDTO"/>
</many-to-one>
<!-- etc., etc. -->
</class>
</hibernate-mapping>
This would then emit ClientDTO (which is required to be a superclass of Client!) as the declared type of the "client" property of CampaignDTO. CampaignDTO would have a "client" instance variable with type "ClientDTO", but would have "getClientDTO" and "setClientDTO" (*not* "getClient" and "setClient"!) getters and setters. The Client class could be required to implement the actual "getClient" and "setClient" getter/setters.
However, Hibernate would still know that Client is the actual persistent object class to load and to persist -- in fact, it's possible that little (or none?!) of the Hibernate runtime would need to change at all!
The overall goal here, again, is to allow the behavior -- and the domain logic classes themselves -- to be kept out of the data transfer objects. So clients of the service layer would only ever see, or need to load, the DTO classes. (This keeps the clients from being exposed to changes in the business logic classes.)
What do you think? Not asking for much, am I? ;-D
Cheers!
Rob