Search code examples
javahibernateormhibernate-4.x

How do I declare a one-to-many Map in Hibernate's XML file?


I'm trying to create a one-to-many Map relationship and I'm having trouble declaring it in the Hibernate (4.x) mapping file. I just... can't figure it out.

The entity classes are something like this, where I want to use IPLogEntry#ip as the map key (it is immutable):

class User {

    private long id;
    private Map<String,IPLogEntry> ipHistory = new HashMap<String,IPLogEntry>();
    ... other fields;

    public void addIPEntry (String ip) {
        IPLogEntry e = ipHistory.get(ip);
        if (e == null) {
            e = new IPLogEntry(this, ip);
            ipHistory.put(ip, e);
        }
        e.doOtherStuff(...);
    }

}

class IPLogEntry {

    private long id;
    private User user;
    private String ip;
    ... other fields;

    IPLogEntry (User user, String ip) {
        this.user = user;
        this.ip = ip;
        ... init other fields;
    }

}

And tables like:

╔═══════════════════╗   ╔══════════════════════════════════════════════════╗
║ users             ║   ║ ip_log                                           ║
╟─────────────┬─────╢   ╟─────────────┬───────────────┬──────────────┬─────╢
║ id (bigint) │ ... ║   ║ id (bigint) │ user (bigint) │ ip (varchar) │ ... ║
╚═════════════╧═════╝   ╚═════════════╧═══════════════╧══════════════╧═════╝

Note that ip_log.ip is both the map key and the IPLogEntry#ip field value.

And after a bunch of hand-waving I've got this failed attempt at a mapping (by the way, I don't need cascade delete support right now... don't ask):

<class name="d13.dao.User" table="users">
    <id name="id"><generator class="native"/></id>
    <map name="ipHistory" cascade="save-update,merge">
        <key column="user"/>
        <map-key column="ip" type="string"/>
        <element type="d13.dao.IPLogEntry"/>
    </map>
    ...
</class>

<class name="d13.dao.IPLogEntry" table="ip_log">
    <id name="id"><generator class="native"/></id>
    <many-to-one name="user" class="d13.dao.User" not-null="true"/>
    <property name="ip" not-null="true"/>
    ...
</class>

That gets me this on initialization:

Error: creating static hibernate session factoryCould not determine type for: 
    d13.dao.IPLogEntry, 
    at table: users_ipHistory,
    for columns: [org.hibernate.mapping.Column(elt)]

I think the IPLogEntry side is right, it's the User side and the usage of map that I'm having trouble with.

I've been staring at:

  • The Hibernate Manual, but I just can't wrap my head around map, map-key, and element.
  • This "tutorial", but that uses a basic String instead of a full object for the element type, and also I can't actually tell what purpose that example code serves so it's hard to relate to.
  • This wikibooks page, but it's JPA mappings not Hibernate.

I can't figure out what to do. So I know this is a basic question, but, what mapping descriptor should I use to make this work?


Solution

  • Well, it's very simple with annotation-based mapping

    @OneToMany(mappedBy = "user", cascade = { PERSIST, MERGE })
    @MapKey(name = "ip")
    private Map<String, IPLogEntry> ipHistory = new HashMap<>();
    

    I'm not experienced in XML mapping, nevertheless it should be:

    <class name="d13.dao.User" table="users">
        <id name="id">
            <generator class="native"/>
        </id>
        <map name="ipHistory" cascade="save-update,merge" inverse="true">
            <key column="user_id"/>
            <map-key column="ip" type="string"/>
            <one-to-many class="d13.dao.IPLogEntry"/>
        </map>
        ...
    </class>
    
    <class name="d13.dao.IPLogEntry" table="ip_log">
        <id name="id">
            <generator class="native"/>
        </id>
        <many-to-one name="user" class="d13.dao.User" column="user_id" not-null="true"/>
        <property name="ip" not-null="true"/>
        ...
    </class>
    

    See Example 7.29. Bidirectional association with indexed collection