Imagine the following two entities. Element
is simple class containing some data:
@Entity
public class Element {
private String data;
public String getData() { return data; }
public void setData(String data) { this.data = data; }
}
Next class, named VersionedElement
, extends Element
and contains diff开发者_StackOverflow社区erent versions along with current version. Here is my "solution":
@Entity
public class VersionedElement extends Element {
private Set<Element> versions;
private Element currentVersion;
@Override
public String getData() {
return getCurrentVersion().getData();
}
@Override
public void setData(String data) {
getCurrentVersion().setData(data);
}
@OneToMany
public Set<Element> getVersions() {
return versions;
}
public void setVersions(Set<Element> versions) {
this.versions = versions;
}
@ManyToOne
public Element getCurrentVersion() {
return currentVersion;
}
public void setCurrentVersion(Element currentVersion) {
this.currentVersion = currentVersion;
}
}
And I don't like what I've written, something wrong with it, too straightforward approach. First of all, in the latter class currentVersion
isn't limited by and has no relation to versions
. Looks like the code is lacking some helper classes, or abstraction level, or JPA annotation technique, or all above. I need an elegant, worthy of JPA manual solution for this simple case.
Any hints, links or code snippets would be appreciated.
if you want a ready-to-rock hibernate entity versioning solution try hibernate-envers. It will make object versioning/auditing a breeze for you. Check the documentation at http://docs.jboss.org/envers/docs/index.html
cheers and good luck!
Element
can have an integer field version
in the object Element
itself, acting as a running count of rows, and is updated by a sequence. When you want the latest, you simply need to order by this field in descending order and fetch the first result.
@Entity
@NamedQueries({
@NamedQuery(name="GetHistory", query = "FROM Element e WHERE e.id = :id"),
@NamedQuery(name="GetLatest", query = "FROM Element e \
WHERE e.id = :id order by e.version"),
})
public class Element {
private String data;
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "SEQ_ELEMENT_VERSION")
private int version;
private int id;
public String getData() { return data; }
public void setData(String data) { this.data = data; }
}
Your solution would work, but having another table for the VersionedElement would be a performance overhead: VersionedElement would have no usefull data except some foreign key columns.
What I would do is simply add Element latest as field to class Element. Then, in the DAO, I would add some methods which perform queries based on this field:
List<Element> getHistory(Element element)...
Element getLatest(Element element)...
JPA also supports the @Version annotation, but that's used for optimistic concurrency control. It still might be used for tracking version numbers though.
Well, i saw your comment on @Ioan Alexandru Cucu's answer
I expect it will be compensated somehow from sql side - less rows involved, more optimized query
According to the mapping shown in your question, if you need to retrieve a fully initialized VersionedElement entity, you need to perform a query like this one
from
VersionedElement v
inner join fetch
v.versions
inner join fetch
v.currentVersion
where
v.id = :id
As you can see you need two joins to retrieve your VersionedElement entity. But Element as well as VersionedElement share the data property. To avoid duplication of code, we can define an abstract class which contains the data needed in both entities, as follows
@MappedSuperclass
public abstract class AbstractElement {
private String data;
public String getData() { return data; }
public void setData(String data) { this.data = data; }
}
The JPA 1.0 specification is straightforward
Both abstract and concrete classes can be entities. Entities may extend non-entity classes as well as entity classes, and non-entity classes may extend entity classes.
We need @MappedSuperclass annotation because its mapping information should be applied to the entities that inherit from it. In our case, Element and VersionedElement.
So we can re-write the Element entity as
@Entity
public class Element extends AbstractElement {}
And to avoid the inner join fetch v.currentVersion, why do not store the data supplied by AbstractElement as an @Embedded property instead of a @ManyToOne property ?
@Embeddable
public class ElementAsEmbeddable extends AbstractElement {}
@Entity
public class VersionedElement {
private ElementAsEmbeddable currentElement;
private Set<Element> versions;
@Embedded
public ElementAsEmbeddable getCurrentVersion() { return currentVersion; }
public void setCurrentVersion(ElementAsEmbeddable currentVersion) { this.currentVersion = currentVersion; }
@OneToMany
public Set<Element> getVersions() { return versions; }
public void setVersions(Set<Element> versions) { this.versions = versions; }
}
Now your query should looks like
from
VersionedElement v
inner join fetch
v.versions
where
v.id = :id
Only one join
To set up the currentVersion property from an Element, you just need to cast Element as a AbstractElement
versionedElement.setCurrentVersion((AbstractElement) element);
There is nothing wrong with your solution, but you might want to use entity listeners to ensure the state of your entities in a more elegant way, I guess with @prePersist and @preUpdate listeners. Or alternatively go for a list instead of a set.
You could just keep a reference to the latest entity in a separate table (with one row). Something like:
@Entity
public class CurrentElement {
@OneToOne
private Element currentVersion;
public static Element getCurrentVersion(EntityManager em) {
return em.createQuery("select x from Element x ").getSingleResult().currentVersion;
}
public static void setCurrentVersion(EntityManager em, Element newVersion) {
em.remove(getCurrentVersion(em));
CurrentElement ce = new CurrentElement();
ce.currentVersion = newVersion;
em.persist(ce);
}
}
精彩评论