Java persistence mapped superclass with optional properties
Asked Answered
H

3

5

I'm using the javax.persistence package to map my Java classes.

I have entities like these:

public class UserEntity extends IdEntity {
}

which extends a mapped superclass named IdEntity:

@MappedSuperclass
public class IdEntity extends VersionEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    // Getters and setters below...    

}

The IdEntity super class extends another mapped super class named VersionEntity to make all entities inherit version properties:

@MappedSuperclass
public abstract class VersionEntity {

    @Version
    private Integer version;

    // Getters and setters below...

}

Why?

Because now I can make generic queries on the IdEntity class for all entities, and it will look like this: (example)

CriteriaBuilder builder = JPA.em().getCriteriaBuilder();
CriteriaQuery<IdEntity> criteria = builder.createQuery(IdEntity.class);

Now to the problem.

Some of my entities will have timestamps like created_at and deleted_at. But not all entities.

I could provide these properties in my entity classes like this:

public class UserEntity extends IdEntity {

    @Basic(optional = false)
    @Column(name = "updated_at")
    @Temporal(TemporalType.TIMESTAMP)
    private Date updatedAt;
}

But as I have a lot of entities, this will make me put a lot of redundant code in all entities that should have timestamps. I wish there was some way I could make the relevant classes inherit these fields in some way.

One possible solution is to create a parallell IdEntity superclass, maybe named IdAndTimeStampEntity and make those entities that should have timestamps inherit from this new superclass instead, but hey that's not fair to my colleague-developers because now they have to know which super class to choose from when writing generic queries:

CriteriaBuilder builder = JPA.em().getCriteriaBuilder();
CriteriaQuery<???> criteria = builder.createQuery(???); // Hmm which entity should I choose IdEntity or IdAndTimeStampEntity ?? *Annoyed*

And the generic entity queries become not so generic..

My question: How can I make all of my entities inherit id and version fields, but only a sub part of all entities inherit timestamp fields, but keep my queries to a single type of entities?

Update #1

Question from Bolzano: "can you add the code which you specify the path(holds table info) for entities ?"

Here is a working example of querying a UserEntity which is a IdEntity

CriteriaBuilder builder = JPA.em().getCriteriaBuilder();
CriteriaQuery<IdEntity> criteria = builder.createQuery(IdEntity.class);
Root<IdEntity> from = criteria.from(IdEntity.class);
criteria.select(from);

Path<Integer> idPath = from.get(UserEntity_.id); //generated meta model
criteria.where(builder.in(idPath).value(id));

TypedQuery<IdEntity> query = JPA.em().createQuery(criteria);
return query.getSingleResult();
Hooknose answered 25/11, 2016 at 10:39 Comment(8)
Maybe I'm undertanding you wrong, but i don't think what you want is possible. You can't make queries on Mapped Superclasses because there is no table for them.Snap
I doubt that there is a clean way to do this unless MixIns find their way into Java.Shilling
@911DidBush Yes you can if you specify a Path to the entity which holds the table information.Hooknose
@Hooknose can you add the code which you specify the path(holds table info) for entities ?Nidanidaros
@Hooknose can you explain what would be the problem of just putting timestamp fields into IdAndTimeStampEntity and making it a child of IdEntity? This way queries for IdEntity will also work on the timestamped version.Modify
don't use inheritance for this.Ideologist
Isn't query.getSingleResult() going to return an instance of just IdEntity, so the only thing accessible is id and version? If the caller wants any other column information from the entity they have to cast it (and know what type to cast it to). This seems like a very narrowly useful feature; and one that could easily be hidden behind a generic helper method rather than a solution that dictates your entire object hierarchy.Vidette
IMHO: the query is wrong because you cannot use MappedSuperclass s as a target of queries.Ecclesiology
V
5

I would pick a solution that didn't enforce a class-based object model like you've outlined. What happens when you don't need optimistic concurrency checking and no timestamps, or timestamps but no OCC, or the next semi-common piece of functionality you want to add? The permutations will become unmanageable.

I would add these common interactions as interfaces, and I would enhance your reusable find by id with generics to return the actual class you care about to the caller instead of the base superclass.

Note: I wrote this code in Stack Overflow. It may need some tweaking to compile.

@MappedSuperclass
public abstract class Persistable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    // getter/setter
}

public interface Versioned {
    Integer getVersion();
}

public interface Timestamped {
    Date getCreated();
    Date getLastUpdated();
}

@Embeddable
public class TimestampedEntity {
    @Column(name = "create_date")
    @Temporal
    private Date created;

    @Column
    @Temporal
    private Date lastUpdated;

    // getters/setters
}

@Entity
public class UserEntity extends Persistable implements Versioned, Timestamped {
    @Version
    private Integer version;

    @Embedded
    private TimestampedEntity timestamps;

    /*
     * interface-defined getters.  getTimestamps() doesn't need to 
     * be exposed separately.
     */
}

public class <CriteriaHelperUtil> {
    public <T extends Persistable> T getEntity(Class<T> clazz, Integer id, SingularAttribute idField) {
        CriteriaBuilder builder = JPA.em().getCriteriaBuilder();
        CriteriaQuery<T> criteria = builder.createQuery(clazz);
        Root<T> from = criteria.from(clazz);
        criteria.select(from);

        Path<Integer> idPath = from.get(idField);
        criteria.where(builder.in(idPath).value(id));

        TypedQuery<T> query = JPA.em().createQuery(criteria);
        return query.getSingleResult();
    }
}

Basic Usage:

private UserEntity ue = CriteriaHelperUtil.getEntity(UserEntity.class, 1, UserEntity_.id);
ue.getId();
ue.getVersion();
ue.getCreated();

// FooEntity implements Persistable, Timestamped
private FooEntity fe =  CriteriaHelperUtil.getEntity(FooEntity.class, 10, FooEntity_.id);
fe.getId();
fe.getCreated();
fe.getVersion();  // Compile Error!
Vidette answered 30/11, 2016 at 15:31 Comment(22)
Very interesting indeed, I will evaluate this approach with my code. As you say, there are definitely some benefits in avoiding the permutation mess, but if this is a solution I can convince my colleagues to implement, I do not know. One detail is the naming of the Helper Util class. I try to avoid both "Util" and "Helper", since they are way too generic. But that has nothing to do with your suggested solution overall. Im giving you a +1 for the @Embedded. That was not even in my mind!Hooknose
+1 for this? correct usage is `ue.getTimestamps().getCreated(); And this will make embed timestamps in all entities that should have timestamps. Its the same redundancy like in question!Endometrium
The code sample specifically calls out that getTimestamps() doesn't need to be exposed separately.. UE will have the getCreated/Updated() method because it is implementing the Timestamped interface. Those getters will interact with the timestamps field which is never exposed externally. A small amount of duplication in getters is not onerous. Especially considering there is no longer a need to duplicate the schema annotations or logic to manage the timestamps.Vidette
Works very well with some adjustments. The overall solution looks clean. The embedded timestamp entity has to be put in all entities that should use timestamps, but it's 2 lines of code which is acceptable. No permutation mess and compile-time support. Overall a nice solution which my team is happy with.Hooknose
@Hooknose 2 lines of code? And getter/setter methods for intefaces in every class: return timstamps.getUpdated() cannot be generated automatically.Endometrium
@Hooknose public <T extends Persistable> T getEntity( is not generic method. You need one method for each interface.Endometrium
@PeterSaly I didn't do a good job explaining this, but I assume that Persistable is the bare minimum interface for a DB model (since it defines the @Id). You could actually have a single base class like IdEntity in the original question and define the generic param to that instead of Persistable. But you don't need a method for each interface you end up making.Vidette
@henrik: if assuming @Version is also part of base class, than no interfaces needed. Better solution is extend IdEntity or IdAndTimeStampEntity without duplicate code.Endometrium
@Hooknose Actually assumption, that Id and Version is part of base class, is in question. The question was not create entities without Id and Version. In this case this is not the correct answer.Endometrium
I would absolutely create entities that don't have version checking. And sometimes they have timestamps. I called that out in the answer. Any immutable data has no need for OCC, but it could have a very real need for timestamps. You insist that an inheritance model is superior to this composition, and I'm not sure I get why. This pattern, along with the generic query, is very flexible, doesn't require downcasting anywhere, establishes a more natural "has a" structure, and is significantly more future proof.Vidette
@Vidette The question was NOT how to create entities without id and version. You answer by modifing assumption in question!Endometrium
@Vidette Read the question carefully one more time. Edit answer and provide generic method query for all interfaces you proposed without downcasting! Adding embeded object is the same like adding Date field in question example, even more code in getter getTimstamp().getDate().Endometrium
@PeterSaly I read the question. The opening of my answer clearly indicates as much. My answer doesn't need a generic query method for all interfaces I proposed and doesn't need downcasting. You tell the query you want a UserEntity and it gives you back a UesrEntity. That entity has the interface methods without downcasting. It's basic OO. I'm sorry you felt the need to downvote something you don't understand when the OP clearly felt it addressed the question. I think we're done here.Vidette
@Vidette You are right. 1. Your answer is just basic usage of interfaces. 2. It does not provide generic query method asked in question.Endometrium
@Vidette Query for retrieving persistable entities is not generic. What if other entity implements ONLY Timestamped interface? Can you use this method for such entities? No.Endometrium
@Hooknose I'm down voting also question because this is clearly not correct answer.Endometrium
There I've edited the code to make Persistable an abstract class instead of an interface if you'd prefer the clarity. This is in line with why I explained in a previous comment. Your point about an entity only doing Timestamped and not Persistable is invalid. Persistable is the bare minimum that JPA needs to function (an @Id field). If you've bought into this pattern you don't have entities that are not Persistable.Vidette
I'm down voting also question because this is clearly not correct answer. @Hooknose I'm sorry I poked the salty bear.Vidette
Version is also bare minimum for that JPA needs to function.Endometrium
Version is also bare minimum for that JPA needs to function. falseVidette
Ok, version is not needed. But this was not asked in @Hooknose s question. The deal was only about timestamps.Endometrium
@Vidette and now if you change Timestamed interface into MappedSuperclass, you don't need embed code in every subclass. No code duplicities. Entities without timestamp will extend Persistable. Entities with timestamp will extend Timestamped. And this is what is in my answer.Endometrium
H
3
@MappedSuperclass
public class IdEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Version
    private Integer version;
}   
@MappedSuperclass
public class IdAndTimeStampEntity extends IdEntity{
    Date created;
}
@Entity
public class UserEntity extends IdAndTimeStampEntity{
    String name;
}
@Entity
public class FooEntity extends IdEntity{...

Pros of this solution:

  1. In simple and clear way uses OOP without need to embed duplicate code implementing intefaces in every subclass. (Every class is also interface)

  2. Optimistic locking version column is mostly used approach. And should be part of base class. Except read only entities like codetables.

Usage:

public <T extends IdEntity> T persist(T entity) {

    if (entity instanceof IdAndTimeStampEntity) {
        ((IdAndTimeStampEntity) entity).setCreated(new Date());
    }

    if (!em.contains(entity) && entity.getId() != null) {
        return em.merge(entity);
    } else {
        em.persist(entity);
        return entity;
    }
}
Heptarchy answered 30/11, 2016 at 11:9 Comment(3)
Interesting! Could you also provide an example that shows how this suggested solution would be used in the typed query? (See update #1 in the bottom of my question for an example query to work with)Hooknose
Exactly the same way like in your example.Endometrium
@Hooknose added persist exampleEndometrium
C
2

I wish there was some way I could make the relevant classes inherit these fields in some way.

You could make a custom annotation @Timed and use an annotation processor to add the timestamp field and annotations, either by using a bytecode manipulation framework or creating a delegating subclass. Or, for example if you use Lombok, create a Lombok annotation.

That way, your team members only have to remember to use the @Timed annotation when you have entities with timestamps. Whether you like such approach or not is up to you.

Chemistry answered 1/12, 2016 at 8:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.