I understand you want a tag
table representing tags for multiple different entities rather than a tag
table + join tables for specific entity types (post_tags
, story_tags
etc.), which is how JPA would map a unidirectional one-to-many by default.
In that case I believe this is what you're looking for.
There are essentially three ways to go around this:
1. @Where + @Any
Use @Where
to restrict matching entities in the Post.tags
collection:
@Entity public class Post {
@Id
private String id;
@OneToMany
@Immutable
@JoinColumn(name = "resource_id", referencedColumnName = "id", insertable = false, updatable = false)
@Where(clause = "resource_type = 'post'")
private Collection<Tag> tags;
}
Then, in Tag
use @Any
to define a multi-targeted association:
@Entity public class Tag {
@Id
private Long id;
private String tag;
@CreationTimestamp
private Instant timeCreated;
@JoinColumn(name = "resource_id")
@Any(metaColumn = @Column(name = "resource_type"), optional = false, fetch = LAZY)
@AnyMetaDef(idType = "string", metaType = "string",
metaValues = {
@MetaValue(value = "post", targetEntity = Post.class),
@MetaValue(value = "story", targetEntity = Story.class),
})
private Object resource;
}
Adding a new Tag
to a Post
is straightforward, just assign the Post
to the Tag.resource
property (same for stories and all the other 'taggable' entities)
(Note that you might want to add a base class/marker interface like Taggable
and use it instead of Object
to restrict the types one may assign to the Tag.resource
property. It should work, but I haven't tested it, so I'm not 100% sure)
2. @Where + explicit join column mapping in Tag
Use the same approach as before for Post
and map the resource_id
and resource_type
columns as explicit properties:
@Entity public class Tag {
@Id
private Long id;
private String tag;
@CreationTimestamp
private Instant timeCreated;
@Column(name = "resource_id")
private String resourceId;
private String resourceType;
}
Creating a new Tag
now requires you to populate resourceId
and resourceType
yourself. This approach makes a lot of sense if you want to treat Post
and Tag
as separate aggregate roots, otherwise it's pretty cumbersome and error-prone, as Hibernate does not help you ensure consistency, you need to manage it yourself.
3. Inheritance + mappedBy
Create separate entities for post tags, story tags etc. with the single inheritance strategy and treating the resource_type
column as the discriminator value:
@Entity
@Inheritance(strategy = SINGLE_TABLE)
@DiscriminatorColumn(name = "resource_type")
public abstract class Tag {
@Id
private Long id;
private String tag;
@CreationTimestamp
private Instant timeCreated;
}
@Entity
@DiscriminatorValue("post")
public class PostTag extends Tag {
@JoinColumn(name = "resource_id")
@ManyToOne(optional = false, fetch = LAZY)
private Post post;
}
@Entity
@DiscriminatorValue("story")
public class StoryTag extends Tag {
@JoinColumn(name = "resource_id")
@ManyToOne(optional = false, fetch = LAZY)
private Story story;
}
This solution has the advantage that, in the 'taggable' entity, you no longer need a 'fake' owning @OneToMany
association but can instead use mappedBy
:
@Entity public class Post {
@Id
private String id;
@OneToMany(mappedBy = "post")
private Collection<PostTag> tags;
}
@Entity public class Story {
@Id
private String id;
@OneToMany(mappedBy = "story")
private Collection<StoryTag> tags;
}
Adding a new Tag
is also simplified (Want a new post tag? Create a PostTag
object. Want a new story tag? Create a StoryTag
object instead). In addition, if you ever want to switch to managing Tag
s using the Post.tags
association (i.e. a unidirectional one-to-many), this approach will be the easiest to convert.
(note that in this scenario, you cannot, of course, rely on Hibernate to generate the schema, because it will then try to create FK constraints on resource_id
column pointing to all the candidate tables)
I created a github repo with all three approaches represented as separate commits. For each approach, there's a test proving that it actually works. Note that the database structure is the same for all three scenarios.
(As a side note, I only now noticed the PRIMARY KEY (resource_type, namespace_id, tag)
part of the table definition, so I have to ask: you do understand this question is asked and answered with a one-to-many association in mind, and not many-to-many, right?
I'm asking because with such a PK definition, at most one post
could have a tag
with a given value of the tag
column - for a given namespace_id
, of course. I'm assuming that's a typo and what you really wanted was a PRIMARY KEY(id)
coupled with UNIQUE(resource_type, resource_id, namespace_id, tag)
)