-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 9 posts ] 
Author Message
 Post subject: Transitively closing generated classes: fun with HB-700
PostPosted: Thu Feb 12, 2004 9:13 pm 
Expert
Expert

Joined: Thu Jan 08, 2004 6:17 pm
Posts: 278
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


Top
 Profile  
 
 Post subject:
PostPosted: Fri Feb 13, 2004 3:07 am 
Hibernate Team
Hibernate Team

Joined: Tue Aug 26, 2003 6:10 am
Posts: 8615
Location: Neuchatel, Switzerland (Danish)
First, nice to see you looking into hbm2java in more detail and see it's powers and weakneses (even though you haven't found a weaknes from my point of view ;)

First, the abstract can be removed today with the "scope-class" meta tag. It is a "bad" name, should probably be called "class-modifiers" ;)

Secondly, generating a totally a separate different class hiearchy should be (or at least were intended) to be handled by configuring the a (new or just a different configured) renderer in the config.xml ....it do not like it to be be controlled by a global switch.

I don't have much time to comment thorougly on the rest of the mail now - but will look into it when I get time ;)

...and yes, the patch is welcome - but I do think it should configurable via config.xml instead.

_________________
Max
Don't forget to rate


Top
 Profile  
 
 Post subject:
PostPosted: Fri Feb 13, 2004 3:10 am 
Hibernate Team
Hibernate Team

Joined: Tue Aug 26, 2003 6:10 am
Posts: 8615
Location: Neuchatel, Switzerland (Danish)
....and if "all" you want is to add "DTO" to the end of class and accesor names then you might simplify your exact usecase to add a "prefix/post-classname" and "prefix/post-accessor" feature...avoiding to have you "pollute" the hbm.xml files ?

(maybe even a "classname-strategy" attribute that points to a regular expression (or a helper class?) ...just a thought

_________________
Max
Don't forget to rate


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 22, 2004 4:27 am 
Expert
Expert

Joined: Thu Jan 08, 2004 6:17 pm
Posts: 278
Sorry it's taken me a week to get back to you -- been busy :-) And no, I definitely don't think any of this is a weakness in hbm2java. I'm just asking for some more options.

Config file: yes, absolutely. But naming strategy hooks? Hmm, I think that's overkill, or at least not quite the pattern I'm describing....

The basic mapping I want (for my domain model) is:

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>


I want to be able to generate this with ordinary hbm2java, for my domain model. This is really exactly what I want for my domain model, already. (There's no "pollution".)

But then I want some other configuration that (as I said) does the following: makes all generated classes concrete; and makes the types of all properties, and the superclasses of all classes, be the meta generated-class (not the ordinary class). So there really isn't any name mangling going on at all here; it's just taking the meta generated-classes and using them throughout the new hierarchy, omitting all mention of the ordinary classes.

So I think your suggestions about classname-strategy or renaming methods aren't quite getting at what I want. (I know the early parts of my loooong rambling post made it sound otherwise -- sorry, I was designing on the fly :-)

Here's a proposal: what if I extended hbm2java to allow this config.xml file to generate both the ordinary hierarchy, and the new generated-classes-only hierarchy?

Code:
<codegen>
    <generate renderer="net.sf.hibernate.tool.hbm2java.BasicRenderer"/>
    <generate
        package="dto"
        use-generated-classes-only="true"
        renderer="net.sf.hibernate.tool.hbm2java.BasicRenderer"/>
</codegen>


I think this might be better than defining an entire new Renderer since the behavior change I want is really pretty minimal. Or, would you prefer this:

Code:
<codegen>
    <generate renderer="net.sf.hibernate.tool.hbm2java.BasicRenderer"/>
    <generate
        package="dto"
        renderer="net.sf.hibernate.tool.hbm2java.GeneratedClassOnlyRenderer"/>
</codegen>


?

Let me know -- I am going to start hacking on this tomorrow :-) (we really need this in our application and not having it is starting to hurt!) I also plan to write a generic DTOMapper class which can convert from the all-DTO hierarchy to the ordinary hierarchy and back again. I could contribute this to Hibernate if you are interested, though I'm not entirely sure where it would go (a tools utility class??? something like DTOMapper or GeneratedClassMapper? hmmm....).

Hope to hear from you (and/or whomever else) soon :-)
Cheers!
Rob


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 22, 2004 7:59 am 
Hibernate Team
Hibernate Team

Joined: Tue Aug 26, 2003 6:10 am
Posts: 8615
Location: Neuchatel, Switzerland (Danish)
+1 on improving BasicRenderer (UNLESS it makes many changes/extensions - then I would rather like it be a subclass of the basicrenderer) ;)

Regarding the "use-generated-classes-only="true"" attribute in the codegen, then I would prefer to use the <param> tag instead of a new attribute.

So it would be:
Code:
<codegen>
    <generate renderer="net.sf.hibernate.tool.hbm2java.BasicRenderer"/>
    <generate
        package="dto"
        renderer="net.sf.hibernate.tool.hbm2java.BasicRenderer">
        <param name="use-generated-classes-only">true</param>
   </generate>
</codegen>
[/code]

_________________
Max
Don't forget to rate


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 22, 2004 7:39 pm 
Expert
Expert

Joined: Thu Jan 08, 2004 6:17 pm
Posts: 278
OK, you got it. Working on it now. I'll have to toss out the previous work I did but that is not a big deal.

Cheers!
Rob


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 22, 2004 9:47 pm 
Expert
Expert

Joined: Thu Jan 08, 2004 6:17 pm
Posts: 278
Huh, looks like Generator.java has no support for <param> subelements right now, is that right? I'll go ahead and add some but there'll be more code changes than I expected :-)

(I'm starting from the 2.0.2 tools source... should I start instead from the CVS source? hmm, I guess I obviously should. ok.)

Cheers!
Rob


Top
 Profile  
 
 Post subject:
PostPosted: Mon Feb 23, 2004 1:04 am 
Hibernate Team
Hibernate Team

Joined: Tue Aug 26, 2003 6:10 am
Posts: 8615
Location: Neuchatel, Switzerland (Danish)
<param> support is in CVS and I'm 99% sure it should also be in the latests tools....but anyhow you should do your stuff against CVS.

_________________
Max
Don't forget to rate


Top
 Profile  
 
 Post subject:
PostPosted: Tue Feb 24, 2004 3:30 am 
Expert
Expert

Joined: Thu Jan 08, 2004 6:17 pm
Posts: 278
Done! I have updated HB-700:
http://opensource.atlassian.com/projects/hibernate/secure/ViewIssue.jspa?key=HB-700
I attached both a patch file (against HibernateExt-20040222) and a complete .zip file of my development tree (including my tiny test case with its output, to clarify what I am after with this patch).

Sorry for the mistakes I made in commenting on my uploads -- haven't used JIRA much, can you tell? ;-)

I hope this meets with your approval. I'll be using it here in our development and if I find anything wrong with it I will update the patch (and will stay current with CVS). Please let me know what you think :-)

Cheers!
Rob


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 9 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.