Reputation: 131
Note: I developed this SSSCE to illustrate my problem. The actual problem is much larger (the individual "records" have a lot more fields and a lot more data and the XML data file has 30K records.)
Given the following XML fragment:
<?xml version="1.0" encoding="UTF-8"?>
<manufacturers>
<manufacturer>
<name>BMW</name>
<manufacturing-countries>
<country code="DE" iso="DEU">Germany</country>
</manufacturing-countries>
<using-countries>
<country code="DE" iso="DEU">Germany</country>
<country code="JP" iso="JPN">Japan</country>
<country code="US" iso="USA">United States</country>
<country code="UK" iso="GBR">Germany</country>
</using-countries>
</manufacturer>
<manufacturer>
<name>Honda</name>
<manufacturing-countries>
<country code="JP" iso="JPN">Japan</country>
<country code="US" iso="USA">United States</country>
</manufacturing-countries>
<using-countries>
<country code="JP" iso="JPN">Japan</country>
<country code="US" iso="USA">United States</country>
<country code="UK" iso="GBR">Germany</country>
</using-countries>
</manufacturer>
</manufacturers>
I have a JAX-B-based parser which reads this XML into the following objects (note that the objects are designed to be immutable; they use a builder pattern to build actual objects):
// @Immutable
public class Country {
private String code;
private String name;
private String isoCode;
private Country() {
// private, no arg constructor
}
// getters elided
// builder elided
}
// @Immutable
public class Manufacturer {
private String name;
// list of countries where manufacturer builds thier product(s)
private List<Country> manufacturingCountries;
// list of countries who use manufacturer's product(s)
private List<Country> usingCountries;
private Manufacturer() {
// private no-arg constructor
}
// getters elided
// builder elided
}
So, in my code I can easily load a list of manufacturers from an XML file:
List<Manufacturer> manufacturers = ManufacturerReader.readManufacturersFromXml(xmlFilePath);
I need to turn around and put this information into a database. So far, I've got this Hibernate mapping file:
hibernate.hbm.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping default-access="field">
<class name="Country"
table="COUNTRIES">
<id
name="code"
length="2"
column="CODE">
<generator class="assigned"/>
</id>
<property
name="name"
length="40"
column="NAME">
</property>
<property
name="isoCode"
length="3"
column="ISO_CODE">
</property>
</class>
<class name="Manufacturer"
table="MANUFACTURERS">
<id
type="string"
column="ID">
<generator class="uuid"/>
</id>
<property
name="name"
length="255"
column="NAME">
</property>
<list
name="manufacturingCountries"
table="MANUF_MANUF_COUNTRY"
cascade="save-update">
<key column="MANUFACTURER_ID"/>
<list-index column="POSITION"/>
<many-to-many class="Country" column="COUNTRY_CODE"/>
</list>
<list
name="usingCountries"
table="MANUF_USING_COUNTRY"
cascade="save-update">
<key column="MANUFACTURER_ID"/>
<list-index column="POSITION"/>
<many-to-many class="Country" column="COUNTRY_CODE"/>
</list>
</class>
</hibernate-mapping>
Given that the parser doesn't care about equality, the Country objects that get created are duplicated within and between the various Manufacturers (e.g., there are three "US" Country objects, two "JP" Country objects, etc.)
When I attempt to save the object graph with Hibernate, I get a NonUniqueObjectException because of the duplicate (equality-wise) Country objects.
Exception in thread "main" org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session: [Country#JP]
at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:190)
at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:143)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.saveWithGeneratedOrRequestedId(DefaultSaveOrUpdateEventListener.java:210)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsTransient(DefaultSaveOrUpdateEventListener.java:195)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:117)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:93)
at org.hibernate.impl.SessionImpl.fireSaveOrUpdate(SessionImpl.java:685)
at org.hibernate.impl.SessionImpl.saveOrUpdate(SessionImpl.java:677)
at org.hibernate.engine.CascadingAction$5.cascade(CascadingAction.java:252)
at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:392)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:335)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:204)
at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:425)
at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:362)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:338)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:204)
at org.hibernate.engine.Cascade.cascade(Cascade.java:161)
at org.hibernate.event.def.AbstractSaveEventListener.cascadeAfterSave(AbstractSaveEventListener.java:475)
at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:353)
at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:203)
at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:143)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.saveWithGeneratedOrRequestedId(DefaultSaveOrUpdateEventListener.java:210)
at org.hibernate.event.def.DefaultSaveEventListener.saveWithGeneratedOrRequestedId(DefaultSaveEventListener.java:56)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsTransient(DefaultSaveOrUpdateEventListener.java:195)
at org.hibernate.event.def.DefaultSaveEventListener.performSaveOrUpdate(DefaultSaveEventListener.java:50)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:93)
at org.hibernate.impl.SessionImpl.fireSave(SessionImpl.java:713)
at org.hibernate.impl.SessionImpl.save(SessionImpl.java:701)
at org.hibernate.impl.SessionImpl.save(SessionImpl.java:697)
at Main.main(Main.java:28)
Short of "manually" de-duplicating these objects in-memory after reading from the XML Parser, how can I resolve this problem?
Assumptions:
Edit 1:
The simple code I've come up with to actually transfer the data:
import java.util.List;
import org.hibernate.Session;
import org.hibernate.Transaction;
public class Main {
public static void main(String[] args) {
List<Manufacturer> manuf = ManufacturerXmlReader.readManufacturersFromXml("manufacturers.xml");
Session session = HibernateUtil.getSessionFactory().openSession();
Transaction tx = session.beginTransaction();
for (Manufacturer m : manuf) {
session.save(m);
}
tx.commit();
session.close();
HibernateUtil.shutdown();
}
}
Edit 2:
When changing session.save()
to session.merge()
, I get
Exception in thread "main" org.hibernate.HibernateException: The class has no identifier property: Manufacturer
at org.hibernate.tuple.entity.AbstractEntityTuplizer.getIdentifier(AbstractEntityTuplizer.java:220)
at org.hibernate.persister.entity.AbstractEntityPersister.getIdentifier(AbstractEntityPersister.java:3876)
at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:233)
at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:84)
at org.hibernate.impl.SessionImpl.fireMerge(SessionImpl.java:867)
at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:851)
at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:855)
at Main.main(Main.java:16)
Edit 3:
If I change the Hibernate mapping file to give the Manufacturer ID element a name
attribute like:
<class name="Manufacturer"
table="MANUFACTURERS">
<id
name="id"
type="string"
column="ID">
<generator class="uuid"/>
</id>
I get the following:
Initial SessionFactory creation failed.org.hibernate.PropertyNotFoundException: field [id] not found on Manufacturer
Exception in thread "main" java.lang.ExceptionInInitializerError
at HibernateUtil.<clinit>(HibernateUtil.java:21)
at Main.main(Main.java:11)
Caused by: org.hibernate.PropertyNotFoundException: field [id] not found on Manufacturer
at org.hibernate.property.DirectPropertyAccessor.getField(DirectPropertyAccessor.java:182)
at org.hibernate.property.DirectPropertyAccessor.getField(DirectPropertyAccessor.java:174)
at org.hibernate.property.DirectPropertyAccessor.getGetter(DirectPropertyAccessor.java:197)
at org.hibernate.tuple.PropertyFactory.getGetter(PropertyFactory.java:191)
at org.hibernate.tuple.PropertyFactory.buildIdentifierProperty(PropertyFactory.java:67)
at org.hibernate.tuple.entity.EntityMetamodel.<init>(EntityMetamodel.java:135)
at org.hibernate.persister.entity.AbstractEntityPersister.<init>(AbstractEntityPersister.java:485)
at org.hibernate.persister.entity.SingleTableEntityPersister.<init>(SingleTableEntityPersister.java:133)
at org.hibernate.persister.PersisterFactory.createClassPersister(PersisterFactory.java:84)
at org.hibernate.impl.SessionFactoryImpl.<init>(SessionFactoryImpl.java:286)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1872)
at HibernateUtil.<clinit>(HibernateUtil.java:17)
... 1 more
If I change the name property/field to be an ID field like
<id
name="name"
length="255"
column="NAME">
<generator class="assigned"/>
</id>
Then I get this:
org.hibernate.ObjectNotFoundException: No row with the given identifier exists: [Country#JP]
at org.hibernate.impl.SessionFactoryImpl$2.handleEntityNotFound(SessionFactoryImpl.java:435)
at org.hibernate.event.def.DefaultLoadEventListener.load(DefaultLoadEventListener.java:233)
at org.hibernate.event.def.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:285)
at org.hibernate.event.def.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:152)
at org.hibernate.impl.SessionImpl.fireLoad(SessionImpl.java:1090)
at org.hibernate.impl.SessionImpl.internalLoad(SessionImpl.java:1038)
at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:630)
at org.hibernate.type.EntityType.resolve(EntityType.java:438)
at org.hibernate.type.EntityType.replace(EntityType.java:298)
at org.hibernate.type.CollectionType.replaceElements(CollectionType.java:508)
at org.hibernate.type.CollectionType.replace(CollectionType.java:575)
at org.hibernate.type.AbstractType.replace(AbstractType.java:176)
at org.hibernate.type.TypeHelper.replaceAssociations(TypeHelper.java:262)
at org.hibernate.event.def.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:589)
at org.hibernate.event.def.DefaultMergeEventListener.mergeTransientEntity(DefaultMergeEventListener.java:389)
at org.hibernate.event.def.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:303)
at org.hibernate.event.def.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:464)
at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:255)
at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:84)
at org.hibernate.impl.SessionImpl.fireMerge(SessionImpl.java:867)
at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:851)
at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:855)
at Main.main(Main.java:16)
Exception in thread "main" org.hibernate.ObjectNotFoundException: No row with the given identifier exists: [Country#JP]
at org.hibernate.impl.SessionFactoryImpl$2.handleEntityNotFound(SessionFactoryImpl.java:435)
at org.hibernate.event.def.DefaultLoadEventListener.load(DefaultLoadEventListener.java:233)
at org.hibernate.event.def.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:285)
at org.hibernate.event.def.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:152)
at org.hibernate.impl.SessionImpl.fireLoad(SessionImpl.java:1090)
at org.hibernate.impl.SessionImpl.internalLoad(SessionImpl.java:1038)
at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:630)
at org.hibernate.type.EntityType.resolve(EntityType.java:438)
at org.hibernate.type.EntityType.replace(EntityType.java:298)
at org.hibernate.type.CollectionType.replaceElements(CollectionType.java:508)
at org.hibernate.type.CollectionType.replace(CollectionType.java:575)
at org.hibernate.type.AbstractType.replace(AbstractType.java:176)
at org.hibernate.type.TypeHelper.replaceAssociations(TypeHelper.java:262)
at org.hibernate.event.def.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:589)
at org.hibernate.event.def.DefaultMergeEventListener.mergeTransientEntity(DefaultMergeEventListener.java:389)
at org.hibernate.event.def.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:303)
at org.hibernate.event.def.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:464)
at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:255)
at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:84)
at org.hibernate.impl.SessionImpl.fireMerge(SessionImpl.java:867)
at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:851)
at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:855)
at Main.main(Main.java:16)
Upvotes: 4
Views: 542
Reputation: 131
It appears that the only way to solve this problem is to have open data objects which Hibernate can fully manage. My solution would have to be either:
Everything I've read says that an ID field is necessary, at least on the DB side and you can't use merge on objects without an ID field in the object.
Unless someone can point me to a better answer, I'll have to leave this as the accepted answer, bad as it is.
Upvotes: 0
Reputation: 7213
You can use merge with Hibernate, I think it fits perfectly to your case, look at this question: Hibernate : Downside of merge() over update()
You can use merge with a transient object (should be your object from XML), and a second object which is the object attached to the session (your object that you get from the session of your Hibernate configuration). If you don't load it explicitly in the session before, calling merge will load it (in fact Hibernate checks level 1 cache).
Then Hibernate takes the transient object, check differences with the attached object and apply changes to the attached object; note that transient fields are not merged.
Merge returns the attached object, so you can safely keep doing changes on it while still being in the Hibernate session.
So in your code:
Manufacturer manufacturerDB = session.merge(manufacturerFromXML);
Where manufacturerDB will be the representation of your database of your manufacturer where you have merged values coming from your XML value.
If you don't want to modify that, another simple solution is to iterate through your manufacturers, then on the countries and replacing each country value (int the manufacturerXML) by a countryDB value (coming from Hibernate by session.load(...), you don't have to worry calling this many times for the same id as Hibernate has level 1 cache, same country from session is a single value thanks to cache so you won't have a NonUnique... exception) and it will work.
Upvotes: 1
Reputation: 5700
Direct answer to your question is surrogate key.
You can think of using surrogate key in your implementation...
Upvotes: 0