-->
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.  [ 4 posts ] 
Author Message
 Post subject: Problems with unset session when using Map
PostPosted: Wed Mar 04, 2009 7:12 am 
Newbie

Joined: Mon Jan 15, 2007 1:18 pm
Posts: 8
Hi

I have a problem mapping a Map in Hibernate 3.3.1GA.

In my scenario I have a table A, let’s say it contains customer information, including a country code for their country. Then I have a table B, which contains the descriptions for those country codes in several languages, so it has the columns language, country code and description.

My idea was to map A->B by using a Map, so I have all the descriptions for the different languages, having the language as the key of the map.

The mapping I have looks as follows (from the entity A):

Code:
    ....
    <property name="countryIso" column="ISO_COUNTRY"
    <map name="countryDescriptions">
        <key column="ISO_CODE" property-ref="countryIso" />
        <map-key formula="LANG" />
        <one-to-many class="B" />
    </map>
    ....


This works fine when I retrieve only one row from A. But if I retrieve several rows and more than one happen to point to the same country, then I get a problem where org.hibernate.engine.StatefulPersistenceContext.addCollection() sees that the collection was already loaded for a previous row and decides to unset the session in the Map that was assigned to the previous row entity.

See the code for that method below:
Code:
   /**
    * Add an collection to the cache, with a given collection entry.
    *
    * @param coll The collection for which we are adding an entry.
    * @param entry The entry representing the collection.
    * @param key The key of the collection's entry.
    */
   private void addCollection(PersistentCollection coll, CollectionEntry entry, Serializable key) {
      collectionEntries.put( coll, entry );
      CollectionKey collectionKey = new CollectionKey( entry.getLoadedPersister(), key, session.getEntityMode() );
      PersistentCollection old = ( PersistentCollection ) collectionsByKey.put( collectionKey, coll );
      if ( old != null ) {
         if ( old == coll ) {
            throw new AssertionFailure("bug adding collection twice");
         }
         // or should it actually throw an exception?
         old.unsetSession( session );
         collectionEntries.remove( old );
         // watch out for a case where old is still referenced
         // somewhere in the object graph! (which is a user error)
      }
   }


where “old”, in my case, is the collection assigned to the first entity, which is definitely still being referenced by the first entity!

What I would have expected is that Hibernate would have realised that this collection had already been loaded for another entity and instead of trying to reload it, it would use a reference to it in the new entity, but this doesn’t seem to be the case.

In the end what I get is a LazyInitializationException when the first entity tries to access the Map, as the session for it is null because of the old.unsetSession( session ) bit.

As you can see, Hibernate's code suggests that if this happens, it’s a user error, but I don’t see why this kind of mapping would be incorrect or how I’m meant to map this kind of relationship in a way that this problem doesn’t happen.

I’ve seen a Jira case (HHH-2862) which seems related to this problem, but the reasons why people are having problems seem different to mine and I wanted to check whether anybody could confirm if what I’m trying to do is correct before posting to that Jira.


Any ideas/suggestions?

Thanks


Top
 Profile  
 
 Post subject:
PostPosted: Wed Mar 04, 2009 12:39 pm 
Newbie

Joined: Mon Jan 15, 2007 1:18 pm
Posts: 8
Note that I have tried to map B as a composite-element too, as follows:

Code:
....
    <property name="countryIso" column="ISO_COUNTRY"
    <map name="countryDescriptions" table="B">
        <key column="ISO_CODE" property-ref="countryIso" />
        <map-key formula="LANG" />
        <composite-element class="BValueObjectType">
          <property name="description" column="DESCR" />
        </composite-element>
    </map>
    ....


This produces the same problem as well.

I've tried mapping it to a bag just to see whether it would work, even when that would mean that I have to iterate over the entries until I find the language I want. This doesn't work either and produces the same problem.

It seems that Hibernate only cares about whether the key to the map (in this case ISO_CODE) already exists for the corresponding collection type in it's "collectionByKey" map. But this seems to make it impossible to have 2 entities pointing to a collection based on the same type/key ... that doesn't look correct.

Help anyone?

Thanks


Top
 Profile  
 
 Post subject:
PostPosted: Thu Mar 05, 2009 2:19 am 
Expert
Expert

Joined: Fri Jan 30, 2009 1:47 am
Posts: 292
Location: Bangalore, India
May be you are right and this is a hibernate bug. But dont you think there is a problem with your domain model. Coz the relation between country description and customer is many-to-many and not one-to-many; one CountryDescription is related to many Customers and 1 Customer have many CountryDescription (in diff lang). So I think if you have a intermediate entity Country then your problem should be solved.

Customer MTO Country
Country OTM CountryDescription

So the mapping wud be like:
Code:
   <class name="Client" table="CLIENTS">
      ..............
      ..............
      <many-to-one name="country" column="ISO_COUNTRY" class="Country" />
   </class>

   <class name="Country" table="COUNTRIES">
      <id name="id" type="string">
         <column name="COUNTRY_ID" length="2" sql-type="char"></column>
         <generator class="assigned" />
      </id>
      <property name="name" column="COUNTRY_NAME" />
      <map name="countryDescriptions" cascade="none" mutable="false">
         <key column="ISO_CODE"/>
         <map-key formula="LANG" type="string" />
         <one-to-many class="CountryDescription"/>
      </map>
   </class>
   
   <class name="CountryDescription" table="COUNTRY_DESC">
      <id name="id" column="ID" unsaved-value="0">
         <generator class="native" />
      </id>
      <property name="countryIso" column="ISO_CODE" />
      <property name="countryLang" column="LANG" />
      <property name="description" column="DESCRIPTION" />
   </class>

_________________
Regards,
Litty Preeth


Top
 Profile  
 
 Post subject:
PostPosted: Thu Mar 05, 2009 12:28 pm 
Newbie

Joined: Mon Jan 15, 2007 1:18 pm
Posts: 8
Thanks Litty,

Unfortunately I'm stuck with a legacy database used by a multitude of processes, so I need to adapt my model to it and I don’t have the flexibility to change the database structure to the approach you propose.

What it’s more, my real case is more complex than what I explained in my first post. My B table is not about country descriptions but a general internationalised description table for many things, being country descriptions just one of them. This gets discriminated by an additional B.TYPE column. A lot of tables use B to get different kind of descriptions, in fact we have thousands of types. If I went for a join table we’d have to create one per table accessing B and we have dozens of those.

But you are correct, the root of the problem is that I'm trying to map a many-to-many relationship as if it was a one-to-many.

After some thinking I've got a working solution that doesn’t require creating new join tables or modify the database, as follows:
a) I’ve created a new entity called BNormaliser, this entity maps to the same table as B and has the same PK. This new entity contains a Map of B and can be used by any table that requires a Map of B
b) I’ve changed the mapping to be A -> BNormaliser -> B

See the new mapping below

Code:
<class name="A" table="A">
    ....
    <many-to-one name="countryDescriptions" class="BNormaliser" not-found="ignore">
      <formula>'EN'</formula>
      <formula>'country'</formula>
      <formula>ISO_CODE</formula>
    </many-to-one>
    ....
</class>


<class name="BNormaliser" table="B">
    <composite-id name="id" class="BId">
      <key-property name="language" column="LANG"/>
      <key-property name="type" column="TYPE"/>
      <key-property name="code" column="CODE" />
    </composite-id></class>

    <component name="foreignKey" class="BId" access="field">
      <property name="type" column="TYPE" insert="false" update="false" />
      <property name="code" column="CODE" insert="false" update="false" />
    </component>

    <map name="descriptions">
      <key property-ref="foreignKey">
        <column name="TYPE" />
        <column name="CODE" />
      </key>
      <map-key formula="LANG" type="string" />
      <one-to-many class="B" />
    </map>
</class>


<class name="B" table="B">
    <composite-id name="id" class="BId">
      <key-property name="language" column="LANG"/>
      <key-property name="type" column="TYPE"/>
      <key-property name="code" column="CODE" />
    </composite-id></class>

    <property name="description" column="DESCR" not-null="true" />
</class>


Basically I select a record from the table B (into BNormaliser) for a language I know I always have a description (in my case, EN), just so I can have my A->BNormaliser working properly as a many-to-one relationship. The fact that BNormaliser will be linked to the EN language is irrelevant. Then I get a map from BNormaliser to B that includes all languages.

As you can see, there is a bit of extra mapping in BNormaliser (the component "foreignKey"), which I had to put in order to use composite primary keys, which unfortunately are quite common in legacy databases.
I’d normally use <formula> for this, but <key> doesn’t allow that and if one uses <key> without property-ref, it requires matching PK columns between both tables, this would have returned a single record because of the EN language in BNormaliser. The fact that you can use property-ref with a component, in order to map to a composite key, has been a major discovery I’ve just come across! But it seems to work fine and opens the door to mappings that looked impossible to me before. I actually used to work with Hibernate filters to achieve this!

Finally, I’ve hidden all this new complexity from my model, by keeping the same interface in A (and the other entities that use B) by doing
Code:
return null == countryDescriptions ? null : countryDescriptions.getDescriptions();
where I used to just return the map before.

Hopefully this solution will help other people with similar problems. It’d be great if Hibernate allowed this kind of relationship straight away, without so much complication. If someone has a neater solution that doesn’t require using a work around like “BNormaliser”, please let me know.


Juan


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 4 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.