-->
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: how to map the value of a lookup table
PostPosted: Sun Feb 26, 2006 9:21 pm 
Newbie

Joined: Wed Nov 26, 2003 4:41 pm
Posts: 12
Hi all,

Its been a little while since I've used hibernate, but I'm currently mapping a bunch of complex hand code SQL that loads/saves JAXB classes to use hibernate. The approach I'm taking is to annotate the JAXB classes themselves, so that both hibernate and JAXB use the same classes. I'm using hibernate 3.11 and annotations 3.1 beta8.

My problem, in a nutshell, is that my XML schema uses a String that corresponds to a lookup table. Basically I have the following tables:

Code:
+-------------+       +------------------+
| deal        |       | deal_status      |
+-------------+       +------------------+
| deal_id     |   +---| deal_status_id   |
| deal_status |---+   | deal_status_desc |
+-------------+       +------------------+


and in my XML I want it to look something like:

Code:
<deal>
  <status>complete</status>
</deal>


I'm trying to map this as a secondary table using the following:

Code:
@SecondaryTables({
   @SecondaryTable(name = "deal_history", pkJoinColumns = {@PrimaryKeyJoinColumn(name = "deal_record_id", referencedColumnName = "deal_record_id")}),
   @SecondaryTable(name = "deal_status", pkJoinColumns = {@PrimaryKeyJoinColumn(name = "deal_status_id", referencedColumnName = "deal_status")})
})


Note that I already have the deal_history secondary table working nicely, but have included it here in case it affects things in some way I'm not aware of.

Then at the field level I have:

Code:
   @Column(table = "deal_status", name = "deal_status_desc")
   protected String dealStatus;


I would have thought that the secondary table annotation would join where deal.deal_status = deal_status.deal_status_id and then allow the column to be populated with the value of deal_status_desc. Instead I get the following stack trace:

Code:
Exception in thread "main" org.hibernate.MappingException: Unable to find column with logical name: deal_status in org.hibernate.mapping.Table(deal) and its related supertables and secondary tables
   at org.hibernate.cfg.Ejb3JoinColumn.checkReferencedColumnsType(Ejb3JoinColumn.java:346)
   at org.hibernate.cfg.annotations.TableBinder.bindFk(TableBinder.java:198)
   at org.hibernate.cfg.annotations.EntityBinder.bindJoinToPersistentClass(EntityBinder.java:375)
   at org.hibernate.cfg.annotations.EntityBinder.finalSecondaryTableBinding(EntityBinder.java:361)
   at org.hibernate.cfg.AnnotationBinder.bindClass(AnnotationBinder.java:662)
   at org.hibernate.cfg.AnnotationConfiguration.processArtifactsOfType(AnnotationConfiguration.java:276)
   at org.hibernate.cfg.AnnotationConfiguration.secondPassCompile(AnnotationConfiguration.java:210)
   at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1156)
   at com.toyota.advance.deal.jaxbhibernate.Scratch.main(Scratch.java:30)


My questions are:

1. Should this approach work?
2. Have I missed anything obvious?
3. Could this be a bug and should I be putting together a stand alone test?

Thanks in advance for any replies.

cheers
dim


Top
 Profile  
 
 Post subject:
PostPosted: Mon Feb 27, 2006 12:11 pm 
Beginner
Beginner

Joined: Fri Jul 30, 2004 2:53 pm
Posts: 33
Location: Washington, DC
Do you have
Code:
@Column(name="deal_status")
on the deal_status property in your Deal object?


Top
 Profile  
 
 Post subject:
PostPosted: Mon Feb 27, 2006 1:11 pm 
Beginner
Beginner

Joined: Fri Jul 30, 2004 2:53 pm
Posts: 33
Location: Washington, DC
Secondary tables can only be used if you are trying to map the PK the table, which in this case is deal.deal_id, but you want to map deal.deal_status. You should use ManyToOne instead:

DealStatus:
Code:
@Entity
@Table(name="deal_status")
public class DealStatus implements Serializable {

    private Long id;
    private String description;

    @Id @Column(name="deal_status_id")
    public Long getId() { return id; }
   
    @Column(name="deal_status_desc")
    public String getDescription() { return description; }
   
    //Generate Setters in IDE...
   
}


Deal:
Code:
@Entity
public class Deal implements Serializable {

    private Long id;
    private DealStatus status;
   
    @Id @Column(name="deal_id")
    public Long getId() { return id; }
   
    @ManyToOne() @JoinColumn(name="deal_status")
    public DealStatus getStatus() { return status; }
   
    //Generate Setters in IDE...
   
}


Top
 Profile  
 
 Post subject:
PostPosted: Mon Feb 27, 2006 7:40 pm 
Newbie

Joined: Wed Nov 26, 2003 4:41 pm
Posts: 12
Hi Paul,

paul.barry wrote:
Do you have
Code:
@Column(name="deal_status")
on the deal_status property in your Deal object?


No, as per my post I have:

Code:
@Column(table = "deal_status", name = "deal_status_desc")
   protected String dealStatus;


because I want the value of that string to map to the deal_status.deal_status_desc column. If I mapped it to the deal.deal_status colum then I'd have the primary key of the lookup table, not the lookup table itself.

cheers
dim


Top
 Profile  
 
 Post subject:
PostPosted: Mon Feb 27, 2006 7:45 pm 
Newbie

Joined: Wed Nov 26, 2003 4:41 pm
Posts: 12
Hi Paul,

paul.barry wrote:
Secondary tables can only be used if you are trying to map the PK the table, which in this case is deal.deal_id, but you want to map deal.deal_status. You should use ManyToOne instead ...

[snip]



The problem with that approach is that the type of the dealStatus member is then a DealStatus object, which is not what I want. I don't want to have to change the structure (which currently has a String type for the dealStatus member). The reason for this is that in the JAXB unmarshall I would then need to know about the database primary key value in order to find it.

I find it hard to believe that the use of lookup tables like this hasn't been addressed - am I asking the wrong questions?

btw - I'm having lot of success in mapping other areas of a fairly complex schema and am very happy with the flexibility provided by hibernate on the whole.

cheers
dim


Top
 Profile  
 
 Post subject: Try using a user defined type...
PostPosted: Wed May 17, 2006 12:07 am 
Newbie

Joined: Sun Mar 12, 2006 7:47 pm
Posts: 5
The way I've been working with these types of tables is to use a UserType. Unfortunately, there's no pretty solution for this out of the box that is provided with the Hibernate distribution. Check out the wiki, in particular:

http://hibernate.org/265.html

You'll really need something along these lines:

Baseclass For Lookup Values:
Code:
public abstract class Lookup
{
  private int id;
  private String desc;

  private static final Map<Class<? extends Lookup>, Map<Integer,Lookup>> instanceIdMap = new HashMap<...>;
  private static final Map<Class<? extends Lookup>, Map<String,Lookup>> instanceDescMap = new HashMap<...>;

  protected static void addInstance(Class<? extends Lookup> clazz, Lookup instance)
  {
    if(instanceIdMap.get(clazz) == null) {
        instanceIdMap.put(clazz, new HashMap<...>());
        instanceDescMap.put(clazz, new HashMap<...>());
    }

    instanceIdMap.get(clazz).put(instance.getId(), instance);
    instanceDescMap.get(clazz).put(instanceId.getDesc(), instance);
  }

  protected Lookup() { }

  protected Lookup(int id, String desc) { 
    this.id = id; this.desc = desc;
    addInstance(this.getClass(), this);
  }

  public static Lookup getInstance(Class<? extends Lookup> clazz, int id)
  {
    if(instanceIdMap.get(clazz) != null) {
      return instanceIdMap.get(clazz).get(id);
    }
    else {
      return null;
    }
  }

  public static Lookup getInstance(Class<? extends Lookup> clazz, String desc)
  {
    if(instanceDescMap.get(clazz) != null) {
      return instanceDescMap.get(clazz).get(desc);
    }
    else {
      return null;
    }
  }

  public static void saveOrUpdate(Class<? extends Lookup> clazz, Lookup updated)
  {
    Lookup instance = getInstance(clazz, updated.id);

    if(instance != null) {
      instance.desc = updated.desc;
    }
    else {
      addInstance(clazz, updated);
    }
  }

  public static void saveOrUpdate(Class<? extends Lookup> clazz, int id, String description)
  {
    try {
      Lookup symbol = clazz.newInstance();

      saveOrUpdate(clazz, symbol);
    } catch (InstantiationException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  public int getId()
  {
    return id;
  }

  public void setId(int id)
  {
    this.id = id;
  }

  public String getDesc()
  {
    return description;
  }

  public void setDesc(String desc)
  {
    this.desc = desc;
  }
 
  public boolean equals(Object obj)
  {
    if(obj == null) {
      return false;
    }

    if(!(this.getClass().isAssignableFrom(obj.getClass()))) {
      return false;
    }

    return this.id == ((Lookup)obj).id;
  }

  public int hashCode()
  {
    return new HashCodeBuilder(17, 37).append(this.id).toHashCode();
  }
}


UserType for Hibernate integration:
Code:
public class LookupUserType
  implements UserType, ParameterizedType
{
  private Class<? extends Lookup> lookupClass;
  private String lookupClassName;

  public void setParameterValues(Properties properties)
  {
    String lookupClassName = (String) properties.get("lookupClassName");

    if(lookupClassName != null && lookupClassName.length() != 0) {
      try {
        lookupClass = Class.forName(lookupClassName).asSubclass(Lookup.class);
      }
      catch(ClassNotFoundException ex) {
        throw new HibernateException(MessageFormat.format(
            "Class {0} specified as lookupClassName could not be found.",
            lookupClassName));
      }
      catch(ClassCastException ex) {
        throw new HibernateException(MessageFormat.format(
            "Class {0} specified as lookupClassName is not a proper subclass of {1}.",
            lookupClassName, Lookup.class.getName()
        ));
      }
    }
    else {
      throw new HibernateException(
          "Type LookupUserType is parameterized. You must specify the " +
              "lookupClassName property and it must reference a class that " +
              "extends Lookup.");
    }
  }

  /** {@inheritDoc} */
  public int[] sqlTypes()
  {
    return new int[] { Types.SMALLINT };
  }

  public Class returnedClass()
  {
    return Lookup.class;
  }

  public boolean equals(Object object, Object object1)
      throws HibernateException
  {
    return object.equals(object1);
  }

  public int hashCode(Object object)
      throws HibernateException
  {
    return object.hashCode();
  }

  public Object nullSafeGet(ResultSet resultSet, String[] strings, Object object)
      throws HibernateException, SQLException
  {
    short id = resultSet.getShort(strings[0]);

    if(Lookup.getInstance(lookupClass, id) == null) {
      Lookup.saveOrUpdate(lookupClass,
          id, "", "");
    }

    return Lookup.getInstance(lookupClass, id);
  }

  public void nullSafeSet(PreparedStatement ps, Object obj, int i)
      throws HibernateException, SQLException
  {
    ps.setInt(i, obj == null ? 0 : ((Lookup)obj).getId());
  }

  public Object deepCopy(Object object)
      throws HibernateException
  {
    return object;
  }

  public boolean isMutable()
  {
    return false;
  }

  public Serializable disassemble(Object object)
      throws HibernateException
  {
    return (Lookup) object;
  }

  public Object assemble(Serializable serializable, Object object)
      throws HibernateException
  {
    Lookup lookup = (Lookup) serializable;

    return Lookup.getInstance(lookupClass, lookup.getId());
  }

  public Object replace(Object object, Object object1, Object object2)
      throws HibernateException
  {
    return object;
  }

  public String getlookupClassName()
  {
    return lookupClassName;
  }

  public void setlookupClassName(String lookupClassName)
  {
    this.lookupClassName = lookupClassName;
  }
}


Example of Lookup subclass:
Code:
@Entity
@Table(name="widget_status")
public class WidgetStatus
  extends Symbol
{
  public static final RequestStatus PENDING =
      new RequestStatus(1, "pending", "Pending");
  public static final RequestStatus PROCESSED =
      new RequestStatus(200, "processed", "Processed");
  public static final RequestStatus FAILED =
      new RequestStatus(300, "failed", "Failed");

  public RequestStatus()
  {
    super();
  }

  public RequestStatus(int id, String name, String description)
  {
    super(id, name, description);
  }

  public static RequestStatus getInstance(String statusName)
  {
    return (RequestStatus) RequestStatus
        .getInstance(RequestStatus.class, statusName);
  }
}


Example of class dependent on lookup subclass:
Code:
@Entity
@Table(name="widget")
@TypeDefs({
@TypeDef(
        name="com.widgerz.domain.WidgetStatus",
        typeClass= LookupUserType.class,
        parameters={@Parameter(name="lookupClassName", value="com.widgerz.domain.WidgetStatus")})
    })
public class Widget
{
...
private WidgetStatus status;

@Type(type="com.widgerz.domain.WidgetStatus")
public WidgetStatus getStatus() { return status; }
public void setStatus(WidgetStatus status) { this.status = status; }
}


I realize the sample above is kinda lame to read, but the general idea is that you are implementing a Flyweight class and then telling Hibernate how to map the key to an actual class value.

It's been working pretty well for me so far. The only downside is having to make all of your "lookup" classes extend from a common base class. But, on the plus side, your code doesn't depend on hibernate at all (except for the annotations, but I used those just so I wouldn't have to also post a config file). One key thing: declare the typedef on the class it is being used by. Declaring it on the class that the type is really managingin (Lookup subclass) doesn't seem to work vis-a-vis annotations.


Top
 Profile  
 
 Post subject:
PostPosted: Wed May 17, 2006 1:01 am 
Newbie

Joined: Wed Nov 26, 2003 4:41 pm
Posts: 12
Yeah that's pretty much what we've got to. The problem (imho) with this approach is that you have database primary keys in the java code.

Thanks anyway,
dim


Top
 Profile  
 
 Post subject:
PostPosted: Wed May 17, 2006 9:38 am 
Newbie

Joined: Sun Mar 12, 2006 7:47 pm
Posts: 5
I understand the issue with the "primary keys in java code". I struggled with it a bit before going down this path as well. In the end, though, I saw some benefit to seeing programmers do things like

order.setOrderStatus(OrderStatus.SHIPPED);

rather than

order.setOrderStatus(10);

Ideally code like that above will hopefully grow into something more data-driven such as:

order.setOrderStatus(getNextOrderStatus());

where you are working from some kind of state machine. But there's a good chance that somewhere in your app someone will want to write code with constants, so having the constants still helps.

One way to sort of "have your cake and eat it too" is to implement an app lifecycle listener (and perhaps a JMX MBean to go with it) that will exploit the fact that the "lookup subclass" is itself an entity and load the entire table (which will probably be relatively small). This will make sure that all of your flyweights are fully loaded. The base class I provided above isn't constrainted to only those values defined as constants, so this works pretty well.


Top
 Profile  
 
 Post subject:
PostPosted: Wed May 17, 2006 9:44 am 
Newbie

Joined: Wed Nov 26, 2003 4:41 pm
Posts: 12
turksheadsw wrote:
Ideally code like that above will hopefully grow into something more data-driven such as...


Yes - but in my case I'm simply persisting a message that has been sent to me, to the transitioning of statuses isn't my concern. My real problem is the screwed up database schema, we are making progress on this one and so hopefully I won't have to worry about this hack for much longer!

cheers
dim


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.