This week's book giveaway is in the Agile and Other Processes forum.
We're giving away four copies of The Little Book of Impediments (e-book only) and have Tom Perry on-line!
See this thread for details.
Win a copy of The Little Book of Impediments (e-book only) this week in the Agile and Other Processes forum!
  • Post Reply
  • Bookmark Topic Watch Topic
  • New Topic

Changing entity type (discriminator value) in JPA

 
Roel De Nijs
Sheriff
Posts: 10662
144
AngularJS Chrome Eclipse IDE Hibernate Java jQuery MySQL Database Spring Tomcat Server
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
I have following class hierarchy using JPA's inheritance type Single Table.



The AbstractType contains the common properties of all subclasses and each subclass has its own specific properties (some types have more than 50 specific properties). This "design" works really great: creating, updating and deleting types is really easy and straightforward. If you persist a Type2 object all Type1 and Type3 properties are automatically set to null (which is exactly what we need for our application).

One of the use cases requires that a Type2 object can be changed to another type, e.g. Type3. Also not really a problem: get the old Type2 instance, create a new Type3 instance, copy all common properties, remove old Type2 instance and persist new Type3 instance will do the job. But the only drawback of this approach: the id of the old Type2 instance and the id of the new Type3 instance are different, requiring updates of all entities which are referring to the old Type2 instance.

So I was wondering if it's possible to change the entity type (discriminator value) and keep the same id? This issue already made me question this class design. Maybe it's not the most appropriate for this use case. So that can be changed too (if needed), but I don't want to sacrifice it completely. For example I don't want to work with just 1 class containing the common properties and all specific properties of all subclasses (reasons should be obvious).
 
Martin Vajsar
Sheriff
Posts: 3752
62
Chrome Netbeans IDE Oracle
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Couldn't you take the ID generation into your own hands completely (ie. omit the @GeneratedValue annotation) and make sure to reuse the ID if the entity is created in the "change entity type" process? I believe you could use the Application Server Controlled Keys technique mentioned in one of Scott's blogs.
 
Roel De Nijs
Sheriff
Posts: 10662
144
AngularJS Chrome Eclipse IDE Hibernate Java jQuery MySQL Database Spring Tomcat Server
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Martin Vajsar wrote:Couldn't you take the ID generation into your own hands completely

That's what I'm trying at this moment. I removed the @GeneratedValue annotation and I'm now using keys generated by the UUID class.

With this approach I can't even add a new instance anymore in my current setup (explained in detail here). If I use the ChildRepository to persist, I get the following exception:
org.springframework.dao.DataIntegrityViolationException: a different object with the same identifier value was already associated with the session: [example.Type2#0c1c7b07-7b8d-4ce9-b08c-5eee572c8440]; nested exception is javax.persistence.EntityExistsException: a different object with the same identifier value was already associated with the session: [example.Type2#0c1c7b07-7b8d-4ce9-b08c-5eee572c8440]
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:313)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:106)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:516)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:754)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:723)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:387)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
at com.sun.proxy.$Proxy44.addPunt(Unknown Source)
at example.TestTypeAdd.execute(TestTypeAdd.java:31)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:88)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
Caused by: javax.persistence.EntityExistsException: a different object with the same identifier value was already associated with the session: [example.Type2#0c1c7b07-7b8d-4ce9-b08c-5eee572c8440]
at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1332)
at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1288)
at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:78)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:512)
... 36 more


Because I know the id before the actual insert, using the ChildRepository becomes obsolete. If I use the ParentRepository, adding a new instance is no problem. Even changing the entity type with preserving the original ID works like a charm (if I flush the ParentRespository after removing the old instance, otherwise I get the same DataIntegrityViolationException). Mission accomplished!

So the only question now: what's the probability of duplicates when using random UUIDs? Maybe it's better to use an AtomicLong or another key generator. Based on this wiki article I would assume using UUID is just fine.
 
Martin Vajsar
Sheriff
Posts: 3752
62
Chrome Netbeans IDE Oracle
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
I would try to avoid the use of UUIDs as primary keys. Not because of possible conflicts, chances of which is really nonexistent, but because they're unnecessarily big in the database (and remember that primary keys appear in indexes and foreign keys too) and also make any inspection of the database using SQL tools cumbersome (it is much easier to read, remember and/or type a four, five, six or seven-digit number than a UUID).
 
Roel De Nijs
Sheriff
Posts: 10662
144
AngularJS Chrome Eclipse IDE Hibernate Java jQuery MySQL Database Spring Tomcat Server
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Martin Vajsar wrote:(it is much easier to read, remember and/or type a four, five, six or seven-digit number than a UUID).

Exactly what I thought when I saw the records in the database But for the presentation tomorrow morning it will be UUIDs. Afterwards I will change to another strategy, not sure which one I'll implement.
 
Bill Gorder
Bartender
Posts: 1682
7
Android IntelliJ IDE Linux Mac OS X Spring
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Changing the type seems a little strange in the inheritance strategy. I am not sure about the function of the objects to know how much sense inheritance makes in your domain. If all it is, is a set of common properties you may be better suited by using an @Embeddable class and using @AttributeOverrides as necessary if the columns are named differently per class. With this approach the properties are stored in the same table as the @Entity which has the @Embedded field. From a Java object perspective the same object is used in all the entities. An example of when you might use this strategy is for an address. In this case an address would define some properties that a corporation and a person both have, however it certainly does not make sense for these two entities to inherit from the same super class.

For obvious reasons probably you have not told us much about the context so some of it will have to be your call on the design decision. I would never generate your own id's. Use that only when you have a natural primary key for example a social security number. Don't use things like UUID's. Use one of the strategies that allows the DB to generate the unique key.
 
Jayesh A Lalwani
Rancher
Posts: 2762
32
Eclipse IDE Spring Tomcat Server
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
I'm with Bill here. Morphing of types just feels so wrong. It's like you are breaking some cardinal rule of OOP Are you sure you want to do that? Are you sure you don;t want to model these objects as differrent "states" rather than as differrent types.

I'm trying to come up with real life examples here to wrap my mind around it. Let's say you have a Caterpillar morphs into a Butterfly. When it;s a Caterpillar, it has some properties, and when it's a Butterfly, it has some properties shared with the Caterpillar that it used to be, and some new properties. One way to do it is have an Insect base class, and a Caterpillar class and Butterfly class that extend Insect. You could then have Caterpillar objects morph into Butterfly objects when they pupate. OR another way to do it (and the way I would do it), is to define a lifecycle property on the Insect class that tells you which stage of life the Insect is in.

 
Roel De Nijs
Sheriff
Posts: 10662
144
AngularJS Chrome Eclipse IDE Hibernate Java jQuery MySQL Database Spring Tomcat Server
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Bill Gorder wrote:Changing the type seems a little strange in the inheritance strategy. I am not sure about the function of the objects to know how much sense inheritance makes in your domain.

Jayesh A Lalwani wrote:I'm with Bill here. Morphing of types just feels so wrong. It's like you are breaking some cardinal rule of OOP Are you sure you want to do that? Are you sure you don;t want to model these objects as differrent "states" rather than as differrent types.

I can elaborate a bit more about the context and the model. It's not a secret. I'm designing/developing a web-application which manages information about the sewerage system. The sewerage system has 2 kinds of objects: points and lines. A point has a bunch of properties and can be (at this moment) 1 of 3 types (hence the given example). Each point type has also a collection of specific properties. For line objects the same applies: some common properties, several types and specific properties per type. Each line has a downstream and/or upstream point.

In the original design you had only 2 entities: Point and Line, both having all possible properties for the kind of object (common properties and specific properties of every type). I was not a huge fan of this "design" for several reasons. Most of them are related by the lack of (some) OO-design. One of the functional requirements is data validation: with just 1 class with all properties this will result (without doubt) in cumbersome code (+1000 LOC, hard to maintain, test & error-prone). That's why I made 1 Point super class and 3 sub classes for the specific types: the super class contains the common properties and each sub class contains only the properties specific for its type (using JPA's DiscriminatorColumn and DiscriminatorValue). And of course a similar design for the line objects.
This design has in my humble opinion much more benefits as the original one and it works like a charm. The only problem I encountered so far, is the one posted here: the user can change the type of a point (or line) object. My gut feeling also made me believe that morphing of types is indeed not the best (OOP) idea. But in this situation I think it could be justified, because of the benefits of this design. But maybe there is some design out there which gives me the best of both worlds.
 
Jayesh A Lalwani
Rancher
Posts: 2762
32
Eclipse IDE Spring Tomcat Server
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Ok, so does the user change the Point, or do they change what is contained in the Point? It seems to me like you are conflating the topology of your system with what is contained in the topology. It seems to me like you should be able to describe that shape of the system seperately. It could be that many of your "shared" properties are the property of the topology rather than the object itself

Trying to come up with an example here (which might be really stupid example). I'm just thinking that let's say I'm modeling the sewage system inside my home. I have a major pipe running between floors, and minor pipes branching out that are connected to the toilet, bath tub, etc. Now is my toilet a Point, or is it connected to a Point. I would tend to think of the toilet as something that is different from a Point. If I redid a bathroom, and moved the toilet and bath tub around, I wouldn't morph the toilet to a bathtub and vice versa. Even if I take the toilet out and seal the pipe, the Point still exists. It's just not connected to nuthin.

Similarly I have the main pipe running between floors. Is that the Line? or does the Line contain the Pipe?. It seems to me that if I replace the I dunno.. copper pipe with gold pipe (hey! this software gig pays so well), I wouldn't morph the Line. I would instead replace the Pipe that is contained inside the Line.
 
Roel De Nijs
Sheriff
Posts: 10662
144
AngularJS Chrome Eclipse IDE Hibernate Java jQuery MySQL Database Spring Tomcat Server
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Jayesh A Lalwani wrote:Ok, so does the user change the Point, or do they change what is contained in the Point? It seems to me like you are conflating the topology of your system with what is contained in the topology. It seems to me like you should be able to describe that shape of the system seperately. It could be that many of your "shared" properties are the property of the topology rather than the object itself

The user does not change the point, he can change the type of the point. So we have 3 types of points: node, inlet and storage-area. Each point has for example an x and an y coordinate. Each type has its own specific properties: e.g. a node has a width and a height; an inlet has a shape and a flow rate; a storage-area only has a volume. The land surveyor (= the user of the application) drives to the construction project (where e.g. a part of the sewerage system is renovated and extended) and measures all relevant parts of the sewerage system (points, lines and their properties). Then he imports this data into our application and this data will be visualized and he can manage all data (create other ones, change property values,...). Finally he execute the validation checking data integrity and some business validations. If he makes a mistake (e.g. point 15 turns out to be a storage-area and not a node) he must be able to change the type of the point and complete another set of properties. For this use case I should be able to change the entity type (or just create a new entity and remove the wrong one).

Jayesh A Lalwani wrote:Similarly I have the main pipe running between floors. Is that the Line? or does the Line contain the Pipe? It seems to me that if I replace the I dunno.. copper pipe with gold pipe

The pipe is a line type (so a subclass of the Line entity). Other types could be weir, vortex, valve, pump,... Copper or gold are (in this context) just a value for the "material" property of the Pipe entity. And a line always has a starting point or an end point or both (so these are common properties of the Line super class). Hopefully that makes some sense
 
Jayesh A Lalwani
Rancher
Posts: 2762
32
Eclipse IDE Spring Tomcat Server
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
From what you describe.. and the way I see it.. a Point has a Node/Inlet/Storage Area. You are seeing the relationship as a Is-a relationship which is why you have the "problem" of morphing types. You shouldn't be using inheritance. You should be using encapsulation. Practically speaking, establishing a has a relationship between Point and Node/Inlet/Storage Area solves the immediate problem of having to morph types. Beyond that, speaking from the POV of modeling the application, a Node/Inlet/Storage Area should be design so they can exist without being connected to the system.
 
Bill Gorder
Bartender
Posts: 1682
7
Android IntelliJ IDE Linux Mac OS X Spring
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Or alternatively if a point has common properties such as x, y coordinates it sounds like it really is kind of like a location. So maybe really what you have is a Node/Inlet/Storage Area that all have a location. This becomes simpler from an ORM mapping perspective as well and goes back to the @Embedded stuff I talked about earlier. Don't forget Joshua Bloch's 'Favor composition over inheritance' rule

On a side note this makes me think of nodes and edges. Makes you want to pull out the Neo4J doesn't it

 
Roel De Nijs
Sheriff
Posts: 10662
144
AngularJS Chrome Eclipse IDE Hibernate Java jQuery MySQL Database Spring Tomcat Server
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Jayesh A Lalwani wrote:From what you describe.. and the way I see it.. a Point has a Node/Inlet/Storage Area. You are seeing the relationship as a Is-a relationship which is why you have the "problem" of morphing types.

To be honest I have thought about this approach. From an OO perspective this is definitely the better design eliminating the need of morphing types. An initial version could be:


But I have no clue about mapping this using JPA annotations to a database model (each PointType having specific properties). That's why I implemented the current design. If I can't solve this issue, I'll have to stick with the current one.
 
Roel De Nijs
Sheriff
Posts: 10662
144
AngularJS Chrome Eclipse IDE Hibernate Java jQuery MySQL Database Spring Tomcat Server
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Bill Gorder wrote:Or alternatively if a point has common properties such as x, y coordinates it sounds like it really is kind of like a location. So maybe really what you have is a Node/Inlet/Storage Area that all have a location.

As far as I know this approach won't solve the type morphing, because you still have a Node/Inlet/StorageArea entity (with a common Location embeddable and you should be able to convert an entity type to another entity type
 
Martin Vajsar
Sheriff
Posts: 3752
62
Chrome Netbeans IDE Oracle
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
And what about the requirement that the ID of the entity must not change when the type changes? Why is this so? To protect referential integrity, or some other reason?
 
Roel De Nijs
Sheriff
Posts: 10662
144
AngularJS Chrome Eclipse IDE Hibernate Java jQuery MySQL Database Spring Tomcat Server
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Martin Vajsar wrote:And what about the requirement that the ID of the entity must not change when the type changes? Why is this so? To protect referential integrity, or some other reason?

A line can have a start point, an end point or both. So if the start point is a Node and it's changed to Inlet, it's actually the same point (just another "type"). From that perspective it would be nice to have the same id, otherwise you'll have to update all lines where that point is a start/end point. And it makes from a non-OO perspective sense: it's still the same point (only type has changed), it's just an update, no need to create a new point (and generate new id).
 
Bill Gorder
Bartender
Posts: 1682
7
Android IntelliJ IDE Linux Mac OS X Spring
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
From my point its not morphing. It is being replaced a node does not change what it is and become a inlet. Rather a inlet replaces the node that was at that location. This problem is really graphy which relational databases are notoriously bad at. (hence my earlier reference to Neo4J)

Any way this is along the lines of what I was talking about



In other words the location may change on a given Node or Inlet entity. And that location may be added to another. However the type of object does not morph. I did not test this or anything but it gives you an idea. I have also attached an image of how this would look from a table perspective if you left it exactly as how I posted it. Its not meant to be a final answer but just to give you a new perspective.
ddl.jpg
[Thumbnail for ddl.jpg]
visualization
 
Jayesh A Lalwani
Rancher
Posts: 2762
32
Eclipse IDE Spring Tomcat Server
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Roel De Nijs wrote:
Jayesh A Lalwani wrote:From what you describe.. and the way I see it.. a Point has a Node/Inlet/Storage Area. You are seeing the relationship as a Is-a relationship which is why you have the "problem" of morphing types.

To be honest I have thought about this approach. From an OO perspective this is definitely the better design eliminating the need of morphing types. An initial version could be:


But I have no clue about mapping this using JPA annotations to a database model (each PointType having specific properties). That's why I implemented the current design. If I can't solve this issue, I'll have to stick with the current one.


Well al you need to do is


and you will need 2 tables.. a Point table and a PointType table. Your inheritance doesn't change.. You are just taking properties that are specific to the pointiness (pointedness??) of the thing, putting it into the Point class and establishing a Has-a relationship between Point and PointType.

I guess Bill is looking at it like the thing contains the Location/Point, whereas I'm looking at it as the Location/Point contains the thing. I think both ways are equally valid based on the information we have here. (except that my way is more valid :p ) Based on what else you need, you might want to think about which approach makes more sense. Funnily, from the database schema POV, it makes no differrence. It's just Foreign keys to the database
 
Bill Gorder
Bartender
Posts: 1682
7
Android IntelliJ IDE Linux Mac OS X Spring
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
I agree both will solve the problem. I like getting rid of any kind of @Inheritance though if its not strictly necessary. I also like to avoid @OneToOne relationships when I can as its usually an indicator that the data belongs in a single table. I still feel that Node and Inlets are types of endpoints that are connected together by lines. Since a point is in essence a location I feel that a node is not a location(or point) but rather a node has a location. This proves there are many ways to solve problems, and like Jayesh said, a lot of it comes down to perspective and preference. I think you have a couple options now that should help you solve your problem without the need to 'morph' objects.
 
Roel De Nijs
Sheriff
Posts: 10662
144
AngularJS Chrome Eclipse IDE Hibernate Java jQuery MySQL Database Spring Tomcat Server
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Great and enlightening discussion resulting in some fresh insights and new perspectives! I'm also convinced both will solve the problem, that's why I awarded a cow for every valuable contribution. I know what my 1st job will be tomorrow morning At this moment I prefer Jayesh's solution a bit more than Bill's.

Bill Gorder wrote:I think you have a couple options now that should help you solve your problem without the need to 'morph' objects.

Because I was slowly becoming a morphing expert, I applied for a position with the Mighty Morphin Power Rangers. Seems I have to withdraw my application now
 
Roel De Nijs
Sheriff
Posts: 10662
144
AngularJS Chrome Eclipse IDE Hibernate Java jQuery MySQL Database Spring Tomcat Server
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Here I am again... with very good news I implemented Jayesh's proposal and all my tests are still green

This new design ticks all the boxes:
  • no "morphing" between objects
  • no need to change the discriminator value
  • using database generated unique keys (no more cumbersome UUIDs)
  • no ID changes anymore when type of Point changes (so no need to change other entities to preserve referential integrity)
  • not 1 class containing all properties, but several classes containing only their specific properties
  • if you persist a Point entity with a certain PointType, all properties of the other PointType entities are aautomatically set to null


  • Very delighted with this new design! Thanks all for the help.
     
    Bill Gorder
    Bartender
    Posts: 1682
    7
    Android IntelliJ IDE Linux Mac OS X Spring
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    Great news, congratulations!
     
    • Post Reply
    • Bookmark Topic Watch Topic
    • New Topic