开发者

JPA - "versioned" entity, design advice needed

开发者 https://www.devze.com 2023-02-20 04:14 出处:网络
Imagine the following two entities. Element is simple class containing some data: @Entity public class Element {

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);
    }
}
0

精彩评论

暂无评论...
验证码 换一张
取 消