I created HB-700 in JIRA two days ago. Last night I started work on a patch for hbm2java to support it. It has turned up all kinds of interesting things. This thread is to discuss those and to give people a chance to recoil in horror and/or help me out :-)
Much of this was already discussed in this thread:
http://forum.hibernate.org/viewtopic.php?t=927833
or in this JIRA entry:
http://opensource.atlassian.com/projects/hibernate/secure/ViewIssue.jspa?key=HB-700
But some new things have turned up. I'm going to recap the whole idea in extreme detail, then get to the new twists that are emerging. I have no idea if this is interesting to anyone else but what the heck, it's open source :-)
So here's a boiled-down version of the basic problem. Say you have two classes, Parent and Child, and there is a subclass of Child:
Code:
<hibernate-mapping>
<class name="my.Parent">
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<set name="children" inverse="true" cascade="all-delete-orphan">
<key column="parent_id"/>
<one-to-many class="my.Child"/>
</set>
</class>
<class name="my.Child">
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<discriminator/>
<many-to-one name="parent" column="parent_id" not-null="true"
class="my.Parent"/>
<subclass name="my.SpecialChild"
discriminator-value="SPEC">
</subclass>
</class>
</hibernate-mapping>
If you run these through hbm2java, you will get (omitting constructors since they're not relevant in this thread):
Code:
package my;
public class Parent {
private Set children;
public Set getChildren () { return children; }
public void setChildren (Set children) { this.children = children; }
}
public class Child {
private Parent parent;
public Parent getParent () { return parent; }
public void setParent (Parent parent) { this.parent = parent; }
}
public class SpecialChild extends Child {
}
Which is all fine, as far as it goes. But now say that you want to add special behavior to Child (such as a "delete" method which deletes it and which removes it from the parent collection). So you want to override the generated class names, so you can write your own extension subclasses. You change the mapping to:
Code:
<hibernate-mapping>
<class name="my.Parent">
<meta attribute="generated-class">my.ParentDTO</meta> <!-- new line -->
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<set name="children" inverse="true" cascade="all-delete-orphan">
<key column="parent_id"/>
<one-to-many class="my.Child"/>
</set>
</class>
<class name="my.Child">
<meta attribute="generated-class">my.ChildDTO</meta> <!-- new line -->
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<discriminator/>
<many-to-one name="parent" column="parent_id" not-null="true"
class="my.Parent"/>
<subclass name="my.SpecialChild"
discriminator-value="SPEC">
<meta attribute="generated-class">my.SpecialChildDTO</meta> <!-- new line -->
</subclass>
</class>
</hibernate-mapping>
This generates:
Code:
package my;
abstract public class ParentDTO {
private Set children;
public Set getChildren () { return children; }
public void setChildren (Set children) { this.children = children; }
}
abstract public class ChildDTO {
private Parent parent;
public Parent getParent () { return parent; }
public void setParent (Parent parent) { this.parent = parent; }
}
abstract public class SpecialChildDTO extends Child {
}
Then you can write:
Code:
package my;
public class Parent extends ParentDTO {
// custom Parent behavior / business logic...
}
public class Child extends ChildDTO {
// custom Child behavior / business logic...
}
public class SpecialChild extends SpecialChildDTO {
// custom SpecialChildDTO behavior / business logic...
}
And these classes stay almost entirely untouched (well, except for constructors) when you modify the mappings and regenerate the DTO classes. Great!
Now, since you are never pleased (actually, since you are
me in this case!), you think to yourself: Users of your system may want to have access to the data in these Parent/Child/SpecialChild entities. But your special business logic methods are not for the likes of them. You want to be able to pass the DTO objects out -- since they contain
just the data -- but you
don't want to expose your customized business logic methods. In fact, you may want to export the DTO classes in some kind of external client .jar file, but you
definitely don't want to expose all your business logic classes in a client .jar file!
In other words, the Hibernate-generated code is in some sense "just plain data beans" -- it really is Data Transfer Objects in the plain sense of the word -- and you want to be able to use it that way.
You can't do this given the above class structure.
First, the classes are all abstract!
Second, the ParentDTO class has a member variable of type Child (not of type ChildDTO).
Third, the SpecialChildDTO class inherits from type Child (not from type ChildDTO). So you can't hand instances of these classes to anyone without exposing type Child, which contains a reference to type Parent, and presto you've dragged your whole class tree out into the open.
The first problem -- everything is abstract -- is a basic fact of the current hbm2java. I've patched hbm2java to support a new attribute, concrete-generated-class="true", which makes generated classes be concrete.
As for the exposure of the domain classes (i.e. the references to Child rather than ChildDTO), you think, there's meta attribute="property-type". Let's try that:
Code:
<hibernate-mapping>
<class name="my.Parent">
<meta attribute="generated-class">my.ParentDTO</meta>
<meta attribute="concrete-generated-class">true</meta> <!-- new line -->
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<set name="children" inverse="true" cascade="all-delete-orphan">
<key column="parent_id"/>
<one-to-many class="my.Child"/>
</set>
</class>
<class name="my.Child">
<meta attribute="generated-class">my.ChildDTO</meta>
<meta attribute="concrete-generated-class">true</meta> <!-- new line -->
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<discriminator/>
<many-to-one name="parent" column="parent_id" not-null="true"
class="my.Parent">
<meta attribute="property-type">my.ParentDTO</meta> <!-- new line -->
<subclass name="my.SpecialChild"
discriminator-value="SPEC">
<meta attribute="generated-class">my.SpecialChildDTO</meta>
<meta attribute="concrete-generated-class">true</meta> <!-- new line -->
</subclass>
</class>
</hibernate-mapping>
This generates:
Code:
package my;
public class ParentDTO {
private Set children;
public Set getChildren () { return children; }
public void setChildren (Set children) { this.children = children; }
}
public class ChildDTO {
private ParentDTO parent;
public ParentDTO getParent () { return parent; }
public void setParent (ParentDTO parent) { this.parent = parent; }
}
public class SpecialChildDTO extends Child {
}
Damn. That SpecialChildDTO class still extends Child. Not good! We want it to extend ChildDTO, right?
No. It
has to extend Child. Because there's no other way to make single inheritance work. If SpecialChild extends SpecialChildDTO (as it must), and if SpecialChildDTO extends ChildDTO (as it must), and if SpecialChild extends Child (as it must), then the only possible arrangement is SpecialChild extends SpecialChildDTO extends Child extends ChildDTO.
Are we doomed? NO. WE ARE NOT DOOMED.
We can generate two *different* hierarchies. The first hierarchy is the normal hierarchy, in which SpecialChildDTO extends Child. This hieararchy never goes over the wire. The
second hierarchy is one which is
completely free of the Parent, Child, and SpecialChild classes. The second hierarchy consists
only of DTO classes. And in the
second hierarchy, SpecialChildDTO directly extends ChildDTO.
In other words, we want a way to control how hbm2java generates its superclass references. I've patched hbm2java some more to allow attribute="extends" to override the default superclass, for subclasses,
only if --extends-overrides-superclass=true. Like so:
Code:
<hibernate-mapping>
<class name="my.Parent">
<meta attribute="generated-class">my.ParentDTO</meta>
<meta attribute="concrete-generated-class">true</meta>
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<set name="children" inverse="true" cascade="all-delete-orphan">
<key column="parent_id"/>
<one-to-many class="my.Child"/>
</set>
</class>
<class name="my.Child">
<meta attribute="generated-class">my.ChildDTO</meta>
<meta attribute="concrete-generated-class">true</meta>
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<discriminator/>
<many-to-one name="parent" column="parent_id" not-null="true"
class="my.Parent">
<meta attribute="property-type">my.ParentDTO</meta>
<subclass name="my.SpecialChild"
discriminator-value="SPEC">
<meta attribute="generated-class">my.SpecialChildDTO</meta>
<meta attribute="concrete-generated-class">true</meta>
<meta attribute="extends">my.ChildDTO</meta> <!-- new line: ignored UNLESS extendsOverridesSuperclass -->
</subclass>
</class>
</hibernate-mapping>
Previously, the extends in my.SpecialChild would have generated a warning and been ignored. Now, if extends-overrides-superclass is set, it actually pays attention to it, in order to keep the DTO hierarchy self-contained (i.e. with no references to non-DTO classes).
If you set --extends-overrides-superclass, this now generates:
Code:
package my;
public class ParentDTO {
private Set children;
public Set getChildren () { return children; }
public void setChildren (Set children) { this.children = children; }
}
public class ChildDTO {
private ParentDTO parent;
public ParentDTO getParent () { return parent; }
public void setParent (ParentDTO parent) { this.parent = parent; }
}
public class SpecialChildDTO extends ChildDTO {
}
No more Child references. No more Parent references. We've got a transitively closed set of classes. We're happy! We can now export this class hierarchy to client machines, and we can construct a mapper to build instances of this hierarchy from instances of the base (domain model) hierarchy.
Fine. So let's go back to the normal world, in which extends-overrides-superclass is false (the default :-). In that world, the above extends clause will be ignored, and the mapping will generate:
Code:
package my;
public class ParentDTO {
private Set children;
public Set getChildren () { return children; }
public void setChildren (Set children) { this.children = children; }
}
public class ChildDTO {
private ParentDTO parent;
public ParentDTO getParent () { return parent; }
public void setParent (ParentDTO parent) { this.parent = parent; }
}
public class SpecialChildDTO extends Child {
}
There's something bad here. In your client code, if you want to get a Child's Parent, you now have to do:
Code:
Parent childsParent = (Parent)child.getParent();
That looks just plain wrong. You want to be able to have getParent and setParent still take Parent objects. In fact, you want to define them in your Child subclass itself, since that's where you have access to Parents themselves! So, you want the generated ChildDTO class to define a
different accessor for the
same field.
This is also not currently possible. So I've patched hbm2java further to support
another new attribute, which looks like this:
Code:
<hibernate-mapping>
<class name="my.Parent">
<meta attribute="generated-class">my.ParentDTO</meta>
<meta attribute="concrete-generated-class">true</meta>
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<set name="children" inverse="true" cascade="all-delete-orphan">
<key column="parent_id"/>
<one-to-many class="my.Child"/>
</set>
</class>
<class name="my.Child">
<meta attribute="generated-class">my.ChildDTO</meta>
<meta attribute="concrete-generated-class">true</meta>
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<discriminator/>
<many-to-one name="parent" column="parent_id" not-null="true"
class="my.Parent">
<meta attribute="property-type">my.ParentDTO</meta>
<meta attribute="accessor-name">ParentDTO</meta> <!-- new line -->
<subclass name="my.SpecialChild"
discriminator-value="SPEC">
<meta attribute="generated-class">my.SpecialChildDTO</meta>
<meta attribute="concrete-generated-class">true</meta>
<meta attribute="extends">my.ChildDTO</meta> <!-- ignored by default -->
</subclass>
</class>
</hibernate-mapping>
This now generates:
Code:
package my;
public class ParentDTO {
private Set children;
public Set getChildren () { return children; }
public void setChildren (Set children) { this.children = children; }
}
public class ChildDTO {
private ParentDTO parent;
public ParentDTO getParentDTO () { return parent; }
public void setParentDTO (ParentDTO parent) { this.parent = parent; }
}
public class SpecialChildDTO extends Child {
}
And you can now write:
Code:
package my;
public class Parent extends ParentDTO {
// custom Parent behavior / business logic...
}
public class Child extends ChildDTO {
// custom Child behavior / business logic...
public Parent getParent () { return (Parent)this.getParentDTO(); }
public void setParent () { this.setParentDTO(parent); }
}
public class SpecialChild extends SpecialChildDTO {
// custom SpecialChildDTO behavior / business logic...
}
That's it. You're done. You now have domain (Parent and Child) classes that build off your DTO generated code. The DTO generated code has all-DTO fields and all-DTO accessors. The domain classes you write can define accessors that refer to other domain classes. And you can switch one hbm2java switch and generate a complete class hierarchy with
no references to your domain classes. A generic mapper/lazy-loader is just a small bit more work, and you have complete confidence that the all-DTO hierarchy will map exactly to your domain hierarchy.
I've verified that this kind of pattern can be applied to a whole class hierarchy with my patched hbm2java.
So. What do you all think? Is this useful? Too complex? Is there a simpler pattern in here dying to get out? I am starting to wonder whether only a small amount of extra work would let you change the kinds of accessors that are generated in the DTO classes... something like a "generate-dto-only" switch which would enable extends-overrides-superclass, concrete-generated-class, and a property-type override. Then this single mapping:
Code:
<hibernate-mapping>
<class name="my.Parent">
<meta attribute="generated-class">my.ParentDTO</meta>
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<set name="children" inverse="true" cascade="all-delete-orphan">
<key column="parent_id"/>
<one-to-many class="my.Child"/>
</set>
</class>
<class name="my.Child">
<meta attribute="generated-class">my.ChildDTO</meta>
<id name="id" type="long" unsaved-value="null" >
<generator class="native"/>
</id>
<discriminator/>
<many-to-one name="parent" column="parent_id" not-null="true"
class="my.Parent">
<subclass name="my.SpecialChild"
discriminator-value="SPEC">
<meta attribute="generated-class">my.SpecialChildDTO</meta>
</subclass>
</class>
</hibernate-mapping>
would generate this code normally:
Code:
package my;
abstract public class ParentDTO {
private Set children;
public Set getChildren () { return children; }
public void setChildren (Set children) { this.children = children; }
}
abstract public class ChildDTO {
private Parent parent;
public Parent getParent () { return parent; }
public void setParent (Parent parent) { this.parent = parent; }
}
abstract public class SpecialChildDTO extends Child {
}
But if you enabled generate-dto-only, it would generate:
Code:
package my;
public class ParentDTO {
private Set children;
public Set getChildren () { return children; }
public void setChildren (Set children) { this.children = children; }
}
public class ChildDTO {
private ParentDTO parent;
public ParentDTO getParent () { return parent; }
public void setParent (ParentDTO parent) { this.parent = parent; }
}
public class SpecialChildDTO extends ChildDTO {
}
In other words, it would make all generated classes concrete, and it would make the generated-class of an entity be the class of all properties which reference -- and the superclass of all classes that subclass! -- that entity class.
I can submit the basic patch this weekend (by Monday night, anyway). Is there interest in my pushing ahead with the generate-dto-only switch? Seems potentially very useful to me... but then again, you know by now that I'm crazy >-D
Cheers!
Rob