views:

627

answers:

1

Hi,

I'm trying to map a collection (of type map) using a foreign key and a fixed value as the key/mapping arguments.

I have several tables of product types and a language table which holds stuff like product names and so on.

Now let's say we have an Accessory table which holds (obviously) accessories, then the name of an accesory is stored in the language table with language.id = accessory.id and language.type='accessory'. The map-key should be the language.lang field, a language-code string.

Now, no matter what I've tried, I just can't get the "language.type='accessory'" part right, doesn't like multiple key element, which in turn wouldn't allow elements anyways.

I've also tried it with a composite component as foreignKey, with the constant set by default, but that didn't really work either:

<class name="AccessoryTypes"
 table="accessorytypes">
 <id name="id" column="id" type="java.lang.Long" unsaved-value="0">
  <generator class="identity"></generator>
 </id>
 <map name="Name" table="ProductCode">
  <key column="CompositeId" />
  <map-key column="Language" type="string" />
  <one-to-many class="ProductCode" />
 </map>
</class>

<class name="Language"
     table="Language">
     <composite-id name="compositeId" class="languageKey">
      <key-property name="Type"></key-property>
      <key-property name="Id"></key-property>
     </composite-id>
     <property name="Lang" type="string"></property>
     <property name="Value"></property>
    </class>

of course with the appropriate classes. This approach gives no error, but doesn't fill the HashMap of the Accessory class either...

any help would be appreciated, thanks.

[edit] I now tried it with property-ref as ziodberg suggested, first with a not with like this:

<class name="AccessoryTypes"
     table="accessorytypes">
    <id name="id" column="id" type="java.lang.Long" unsaved-value="0">
     <generator class="identity"></generator>
    </id>
    <properties name="CompositeId" >
     <property name="id" />
     <property name="Type" formula="'accessory'" />
    </properties>
    <map name="Name" table="ProductCode">
     <key property-ref="CompositeId" />
     <map-key column="Language" type="string" />
     <one-to-many class="ProductCode" />
    </map>
</class>

and

<class name="com.swissclick.wesco.web.model.ProductCode"
     table="ProductCode">
    <composite-id  class="com.swissclick.wesco.web.model.ProductCodeKey" mapped="true">
     <key-property name="Type"></key-property>
     <key-property name="id"></key-property>
    </composite-id>
    <property name="Language" type="string"></property>
    <property name="Value"></property>
</class>

but this doesn't work, either, it gives

org.hibernate.MappingException: collection foreign key mapping has wrong number of columns: AccessoryTypes.Name type: component[Id,Type]

which doesn't turn up any helpful information on google.

any ideas?

+1  A: 

Edited. So I'm still not quite clear on your table structure - that's what has thrown me when I wrote the initial answer.

My understanding is that your Language table look something like:

Id -- this is a PK of entity (e.g. Accessory) this entry provides localization info for
Type -- string constant describing entity type (e.g. "accessory")
Language -- language code
Value -- actual localized string

Does that look about right? If so, this raises the question on how you intend to localize multiple fields within the same entity. Shouldn't you at least have another column linking to property (field) name?

At any rate, <formula> is indeed not supported as part of the <key> (I could have sworn that is is) so your options here are pretty limited:

  1. Use a custom loader - this is probably the most straightforward approach.
  2. Add type as an actual column on your Accessory table and map it as part of composite id; you can then map Language map using composite key. Should work but is rather ugly.
  3. Rethink your approach. Do you really need to load a map of languages every time? Seems like doing it as a separate query (or even direct get() assuming appropriate composite id) for current language would be easier and (with adequate caching configuration) a lot faster.

Update To elaborate on #3 above:

First of, let me clarify this - I'm assuming your localized strings are user-editable at runtime. If that's not the case, I'd strongly recommend forgoing the table altogether and going with resource bundles instead.

I'd map Language as a separate entity with type, id, language and (if you need to do this for multiple properties) property_name as parts of composite id. The actual localizable property on your entity (e.g. ProductName on AccessoryType) would be transient. You can then manually load the appropriate localized string for your entity by doing something like:

AccessoryType accessory = ...;
Language language = (Language) session.get(Language.class,
 new LanguageKey("accessory", accessory.id, "en_US", "productName"));
accessory.setProductName(language.getValue());

You can even do that in the event listener assuming that currently selected language is globally obtainable (via global or thread local call) - it will then happen auto-magically for all you entities. If the language table is reasonably small, you can have Hibernate cache it and you won't even incur database hits on this.

ChssPly76
that was actually the first thing I tried, but it doesn't seem work in Hibernate 3, the key-element doesn't take a formula-element.So this leads to an "The content of element type "key" must match "(column)*"." error.
Zenon
yes, that's what the language table looks like. Well, performance isn't really an issue (and never will be, this much I know), so I thought it would be quite elegant if all the database objects just carry their different internationalized strings with them (also, there's only 3 different languages).I guess I'll probably go with option 1., but could you please elaborate 3.?What exactly do you mean by "direct get()" ?Oh and thanks in advance for your efforts, this problem has been driving me nuts over the last few days...
Zenon
thanks a lot for your help, I don't think I could have figured this out any time soon myself. It is my first Java/Hibernate/Spring Project, after all.I would have gone with resource bundles right away had the customer not explicitely requested a database.The get() method with the event listener sounds great, a little more work now but a lot less work in the long run.I was wondering, is there a way to make event-listeners only listen to events concerning certain classes (+derived)?I'll update my question with my solution once I'm finished, if anyone else ever encounters the same problem.
Zenon
You're very welcome. For core Hibernate listener is only registered globally (that is, per session or session factory but for **ALL** entities). However, the easy trick here is to create an interface with callback method, have interested entity classes implement it and then check for "instanceof" in your listener and invoke the method. You can also do this with reflection alone (via predefined method signature) without extra interface. If you were to use Hibernate EntityManager instead, this would be done for you - you'd just annotate the method on entity as `@PostLoad` and it'd be invoked.
ChssPly76