users@glassfish.java.net

Re: Bug in TopLink

From: Markus KARG <markus.karg_at_gmx.net>
Date: Thu, 03 Jan 2008 15:11:21 +0100

Marina,
> It's possible but you need to map the same column twice - once as an
> id, and another time as the relationship field. Look at the
> Order-LineItem example in the Java EE 5 Tutorial.
Thank you for your kind help. In fact it works for me now, but since
there is some more tricks to know, I am posting my solution here for all
those other users facing the same problem. Maybe somebody also likes to
add this solution to the FAQ page.

Problem: You want to have a primary key that contains an entitiy
relationship. JPA 1.0 does not allow entity relations to be part of a
foreign key.

Solution: You can workaround that by a combination of tricks that all
are JPA 1.0 compliant. One possible solution is described below.

Step 1: Duplicate your data field in the entity class, replacing the
class type (like "Customer") of the relationship field by its primary
key's generic data type (like "int"). If the case of compound primary
keys, you need to add several fields, obviously. Since no two java
fields can have the same name, you need to use the @Column(name = ...)
annotation at least for one of them to ensure both map to the same
database column. Note that some database systems support case sensivity,
so ensure to write the name value in the above annotation all in UPPER
CASE to match TopLink's default behaviour, or use the annotation for
both data fields with exactly the same string. Do not mix cases, you
might end up with problems on case sensitive databases (and no, TopLink
is case sensitive and will not prevent this type of problems -- it will
just think of two different fields if you mix cases).

Step 2: Do not mark the Entity relationship field as @Id, but just do
that with the duplicates (the generic java types). If you already marked
the Entity relationship as @Id, remove that annotation now. This is
needed to express that this relationship field no more is a part of the
primary key, but instead the generic type field is to be used.

Step 3: Mark the Entity relationship as read-only, using the annotation
@JoinColumn(updatable = false, insertable = false). If you do not do
that, TopLink will complain about the fact that two fields are mapped to
the same column. That is not allowed. Actually I do not know whether
this is a JPA constraint or a TopLink constraint. Anyways, it will not
work without.

Step 4: Add code to initialize the additional field(s). Lucky me,
primary keys may not be updated according to JPA, so the only thing we
have to keep in mind is the constructor so nothing is to do for UPDATEs.
For SELECTs, TopLink will do the work for us. For DELETEs, obviously
doing anything would be nuts. So we can just concentrate on INSERTs
which means, we have to copy the correct values to the new fields inside
of the constructor. Just set the new generic type field(s) to the value
of the relationship's primary key field(s). Certainly this implies that
you can access that field what, in some cases, means to add a public
getter for that field, unless you already had one.

To summarize, here is a before and after comparison:

Before (not working):

@Id
@ManyToOne
private Customer customer; // Our entity's primary key is a relationship
at the same time

public Invoice(Customer receiver) {
    customer = receiver;
}

After (working):

@JoinColumn(updatable = false, insertable = false)
@ManyToOne
private Customer customer; // Our relationship no more is the primary
key of our entity

@Id
@Column(name = "CUSTOMER_ID")
private int customerId;

public Invoice(Customer receiver) {
    customer = receiver;
    customerId = receiver.getId(); // Additionally storing the id of the
customer.
}

That's all. At least on my PC that worked pretty well, even with this
trick done inside of the head of a JOINED inheritance tree.

BTW, here is another good tip that nicely pairs up with the above steps:
Sometimes the relationship has a primary key made up of several fields.
Adding all of them to your entity is needed as I told above. But that
clutters your code. So what I did, and what I suggest here, is to model
the primary key fields as an embeddable class. To do that, here are some
more steps:

Step 5: Remove all @Id annotations. You don't need them anymore.

Step 6: Write a primary key class (a POJO). Don't forget the public
no-args constructor, the hashCode and equals methods! Useful is a second
constructor that makes up the internal fields from provided parameters
(the you don't need to make them public).

Step 7: Annotate it as @Embeddable.

Step 8: Remove all the PK fields from your entitiy, add them to the
primary key class. Remember not to use @Id anymore.

Step 9: In your entity, add one single field for the primary key,
annotated by @EmbeddedId.

See this sample:

public class Invoice {

    @EmbeddedId
    private InvoicePrimaryKey primaryKey;
 ...
}

@Embeddable
public class InvoicePrimaryKey {

    public InvoicePrimaryKey() {
        // Solely used by the O/R mapper internally when doing SELECTs.
    }

    public InvoicePrimaryKey(Customer customer) {
        // Solely used for our own convenience.
        this.customerId = customer.getId();
      ...more fields here...
    }

    @Column(name = "CUSTOMER_ID")
    @SuppressWarnings("unused")
    private int customerId;

    @Override
    public boolean equals(Object object) {
        if (!(object instanceof InvoicePrimaryKey))
            return false;

        final InvoicePrimaryKey that = (InvoicePrimaryKey) object;

        return that.customerId == this.customerId && ...;
    }

    @Override
    public final int hashCode() {
        return this.customerId ^ ...;
    }

In fact, from my point of view, that's the nicer approach to handling
PKs anyways. ;-)

I hope that in JPA 2.0 this all is much easier and this tricks are
getting obsolete. I have field a request to address this issue to the
specification lead. If you want to support me, write her a nice email
and ask for improvement in this area. You can find her email address in
the JSR of JPA 2.0. :-)

Have Fun
Markus

> Markus KARG wrote:
>> As a result, that means that it is impossible to use JPA to access
>> databases that have a foreign key constraint inside of the primary
>> key. Or did I oversee a possible solution other than replacing the
>> entity reference by a set of simple generic types?
>>> My understanding is that putting @Id on a relationship field is not
>>> supported by version 1.0 of the JPA specification. If you look at
>>> section 2.1.4 of the specification it is quite specific about only
>>> allowing primary keys to be composed of primitive types:
>>>
>>> "The primary key (or field or property of a composite primary key)
>>> should be one of the following types:
>>> any Java primitive type; any primitive wrapper type;
>>> java.lang.String; java.util.Date;
>>> java.sql.Date. In general, however, approximate numeric types (e.g.,
>>> floating point types) should
>>> never be used in primary keys. Entities whose primary keys use types
>>> other than these will not be portable.
>>> If generated primary keys are used, only integral types will be
>>> portable. If java.util.Date is
>>> used as a primary key field or property, the temporal type should be
>>> specified as DATE."
>>>
>>> I have heard some rumors that this is under discussion for the next
>>> version of the specification, but to be honest I am not sure. I
>>> suggest sending a request to the expert group. I am not sure what
>>> the exact email address is, but perhaps someone on this forum will
>>> be nice enough to post it.
>>>> I think I found a bug in TopLink and want to discuss with you
>>>> before hastily reporting it officially.
>>>>
>>>> There is one entity that has a compound key made up of two fields,
>>>> and it has a sub-entity using @Inheritance(strategy = JOINED). I
>>>> can insert new rows using the super class directly, but when I want
>>>> to use the sub-class, TopLink produces a SQL INSERT statement that
>>>> just fills one of the compound key's fields -- and forgets the
>>>> other one! Certainly the database server complains about that.
>>>>
>>>> In fact, I'd like to know whether that is a real bug or whether I
>>>> am doing something wrong (in that case, please tell me what I am
>>>> doing wrong and where in the JPA specification that is told).
>>>>
>>>> Here is the interesting part of the source code:
>>>>
>>>> @Entity
>>>> @Inheritance(strategy = JOINED)
>>>> public abstract class TheSuperClass {
>>>>
>>>> @Id
>>>> @ManyToOne
>>>> private Invoice invoice; // TheSuperClass actually is part of an
>>>> Invoice
>>>>
>>>> @Id
>>>> private int position; // Inside of the Invoice, we identify
>>>> multiple instances using this int
>>>>
>>>> ...
>>>>
>>>> @Entity
>>>> public final class TheSubClass extends TheSuperClass { //
>>>> TheSubClass is a specialization of TheSuperClass
>>>>
>>>> (there is no additional code in this class, since it I stripped
>>>> away everything for this small sample)
>>>>
>>>>
>>>> I am totally stuck with that problem. Please help me. :-)
>>>>
>>>> Thanks
>>>> Markus
>>>>
>>>
>>> ---------------------------------------------------------------------
>>> To unsubscribe, e-mail: users-unsubscribe_at_glassfish.dev.java.net
>>> For additional commands, e-mail: users-help_at_glassfish.dev.java.net
>>>
>>
>>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: users-unsubscribe_at_glassfish.dev.java.net
> For additional commands, e-mail: users-help_at_glassfish.dev.java.net
>


-- 
http://www.xing.com/go/invita/58469