Premise:
I have been recently assigned to work with a Java + JPA + Hibernate application.
This application has 4 different "modules" and each is a "copy+paste" of each other, with minor changes.
I want to remove all duplications and work with a single database schema (currently, there is one schema for each "module").
I am trying to start in the least "invasive" way, not changing too much of what's already there.
What I did was:
I created a base module and moved some hibernate entities there.
I made these entities abstract and created implementations for each module.
I create a new schema and moved every other module record to it (I had to disable the database constraints for now).
Example:
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "SYSTEM", length = 10, discriminatorType = DiscriminatorType.STRING)
#Table(name = "GROUP")
public abstract class Group<U extends UserGroup> {
#Id
private Integer id;
#Column(name = "CODE")
private String code;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "group")
private List<U> users;
#Entity
#Table(name = "USER_GROUP")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "SYSTEM", length = 10, discriminatorType = DiscriminatorType.STRING)
public abstract class UserGroup<G extends Group> {
#Id
private Integer id;
#Column(name = "name")
private String name;
#ManyToOne(optional=false)
#JoinColumn(name = "GROUP_ID", referencedColumnName = "ID", insertable=false, updatable=false)
private G group;
The implementations just define a discriminator column.
Issue:
The following query:
public interface UserGroupRepository<T extends UserGroup> extends CrudRepository<T, Integer> {
#Query(value = "select grp.code from #{#entityName} ug join ug.group grp where ug.name= ?1")
Iterable<String> listGroupByUser(String name);
Is returning 4 items because my user has a record for each module in the database (it should return only 1 item).
Question:
Using "#Query", can I somehow filter by the discriminator value properly?
According to Hibernate Inheritance doc, you can get this by table per subclass instead of table per class hierarchy strategy.
Yes you can try using the where clause. There is a special class property that you can use to restrict a query to a subtype. Below is the hibernate reference documentation.
In your case you can use the subEntity name instead of DomesticCat.
Click here to see the hibernate documentation of the where clause.
Related
I want to create a tree structure where each node can have multiple parents and children. (So actually it is not really a tree but more of a network).
For example, we have an interface to implement the composition, a User class which is the leaf node and a Group class which builds the structure. There would be some check against recursion (adding a group to a group that had the first group as a parent somewhere).
interface GroupMember {
boolean isLeaf();
}
class User implements GroupMember {
private int id;
private String name;
boolean isLeaf() { return true; }
}
class Group implements GroupMember {
private int id;
private Set<GroupMember> members;
boolean isLeaf() { return false; }
public addMember(GroupMember newMember) {
// Some check against recursion
members.add(newMember);
}
}
I see the most efficient way of implementing this in the database would be to have a link table (though this is just a suggestion and not required):
TABLE GROUP_MEMBER
-------------------
PARENT_ID NUMBER
CHILD_TYPE CHAR(1)
CHILD_ID NUMBER
However, I am not sure if Hibernate supports this design. It seems to me that in loading the members set in Group Hibernate would have to consider the discriminator in the GROUP_MEMBER table to decide which class to instantiate.
I have considered having group containing two sets to separately fetch the groups and users, but this seems less than ideal.
May be I'm wrong, but I don't agree with having CHILD_TYPE to be part part of GROUP_MEMBER. I's a CHILD implementation detail and should stay with it. By moving it to the CHILD table, you can use standard ManyToMany JPA mapping, which should make the life simpler.
If desired, CHILD_TYPE can be a discriminator inside the CHILD table.
I always recommend to have a FK. Bugs happen, and orphans in the database are always a huge headache.
Entities:
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "CHILD_TYPE", length = 1)
#Table(name = "MEMBERS", schema = "mtm")
#Data //lombok
#EqualsAndHashCode(onlyExplicitlyIncluded = true) //lombok
public abstract class GroupMember {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Integer id;
#ManyToMany
#JoinTable(name = "GROUP_MEMBER", schema = "mtm",
joinColumns = #JoinColumn(name = "MEMBER_ID", referencedColumnName = "ID"),
inverseJoinColumns = #JoinColumn(name = "PARENT_ID", referencedColumnName = "ID"))
private Set<Group> parents = new HashSet<>();
public abstract boolean isLeaf();
}
#Entity
#DiscriminatorValue("G")
#Data
#EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
class Group extends GroupMember {
#ManyToMany(mappedBy = "parents")
private Set<GroupMember> members = new HashSet<>();
public boolean isLeaf() {
return false;
}
}
#Entity
#DiscriminatorValue("U")
#SecondaryTable(name = "USERS", schema = "mtm")
#Data
#EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
class User extends GroupMember {
#EqualsAndHashCode.Include
#Column(table = "USERS")
private String name;
public boolean isLeaf() {
return true;
}
}
Schema:
create schema if not exists MTM;
CREATE TABLE MTM.MEMBERS (
id INT GENERATED BY DEFAULT AS IDENTITY,
CHILD_TYPE CHAR(1)
);
CREATE TABLE MTM.GROUP_MEMBER (
member_id INT,
parent_id INT
);
CREATE TABLE MTM.users (
id INT,
name varchar(255)
);
Notes:
Standard Hibernate MTM and inheritance strategies are implemented
Common data is stored in the MEMBERS table and User specific inside USERS table (implemented using #SecondaryTable)
Group data is stored entirely inside MEMBERS for efficiency (eliminates JOIN), but can be extended in the same way as User
If required, an additional interface can be introduced for the isLeaf() property.
I think you could use a #NamedQuery looking like select g from Group g left join fetch g.members on top of your Group class and use this query with the Hibernate session. Then you would use a query like select g from Group g left join fetch g.members where g.id = :id and get the result then.
I have an application that loads data related to a sewer network. Here is a sample of entities involved. First, I define a Network entity:
#Entity
#Table(name = "network")
public class Network extends Serializable {
#Id
#generatedValue(strategy=GenerationType.AUTO)
private Long id;
...
#OneToMany(mappedBy = "network")
private List<Conduit> conduits;
#OneToMany(mappedBy = "network")
private List<ConduitVolumeter> conduitVolumeters;
...
}
Second entity involved is Conduit, which represents a pipe in the network.
#Entity
#Table(name = "conduit")
#NamedQuery(name = "Conduit.findAll", query = "SELECT c FROM Conduit c")
public class Conduit extends Serializable {
#Id
#generatedValue(strategy=GenerationType.AUTO)
private Long id;
...
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id_network", nullable = false)
private Network network;
#OneToOne(mappedBy = "conduit", fetch = FetchType.LAZY)
private ConduitVolumeter conduitVolumeter;
...
}
Next, I define ConduitVolumeter. This represents a measuring device used to measure volume in a conduit. Since volumes can be measured in other network items, there is an abstract class Volumeter associated to the volumeter table in the database.
#Entity(name = "ConduitVolumeter")
#DiscriminatorValue("1")
public class ConduitVolumeter extends Volumeter {
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id_nme")
private Conduit conduit;
...
}
#Entity
#Table(name = "volumeter")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "nme_type", discriminatorType = DiscriminatorType.INTEGER)
public abstract class Volumeter extends Serializable {
#Id
#generatedValue(strategy=GenerationType.AUTO)
private Long id;
...
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id_network", nullable = false)
private Network network;
...
}
The problem is that when I try to load all conduits associated to a network, Hibernate tries systematically to load every volumeter associated with each loaded conduit, one at a time, according to its logs.
Hibernate:
select
conduit0_.id as id_1_7_,
conduit0_.name as name23_7_,
conduit0_.id_network as id_netw39_7_,
from
conduit conduit0_ cross
join
network network1_
where
conduit0_.id_network=?
...
Hibernate:
select
conduitvol0_.id as id2_116_0_,
conduitvol0_.name as name10_116_0_,
conduitvol0_.id_network as id_netw15_116_0_,
conduitvol0_.id_nme as id_nme17_116_0_
from
volumeter conduitvol0_
where
conduitvol0_.id_nme=?
and conduitvol0_.nme_type=1
My question : why does Hibernate try to load volumeter data? Each #ManyToOne and #OneToOne annotations is set with FetchType.LAZY. What did I miss?
BTW, I'm asked to use JPA with a legacy database. Is the database model the problem? Is there a way to load the conduits without their volumeters?
Regards.
Francois
FetchType is a just a hint. It depends upon how you are querying the data, if you are using spring-data-jpa repository methods like findOne or findBy methods(derived queries) then Lazy should work, but if you are using JPA repository with #Query on repository method and write your query then Lazy might not work.
I have DisseminationArea as subcalss for Feature with the following code:
#Entity
#Table(name = "features")
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn(name = "subtype_id", discriminatorType = DiscriminatorType.INTEGER)
public class Feature {
#Id
#Column(name="id")
#GeneratedValue(generator="sqlite")
#TableGenerator(name="sqlite", table="sqlite_sequence",
pkColumnName="name", valueColumnName="seq",
pkColumnValue="features")
#Getter
#Setter
private long id;
#ManyToOne
#JoinColumn(name = "subtype_id")
#Getter
#Setter
private FeatureSubtype featureSubtype;
#ManyToOne
#JoinColumn(name = "parent_id")
#Getter
#Setter
private Feature parent;
...
}
Unfortunately, this causes an exception when save this entity to database, because subtype_id field is used twice.
Can I annotate it somehow so that JPA know it is the same field?
If a discriminator column makes sense with InheritanceType.JOINED is worth discussing. I would tend to omit it on joined strategy and only use it with InheritanceType.SINGLE_TABLE.
Nevertheless, it's possible to map it without duplicate column errors.
If your superclass entity has an inheritance / discriminator definition like:
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "subtype_id", discriminatorType = DiscriminatorType.INTEGER)
You just have to adjust the mapping to not update the value as usual by setting it readonly:
#Column(name="subtype_id", insertable = false, updatable = false)
protected int subTypeId;
public int getSubTypeId() {
return subTypeId;
}
Now you can access the discriminator value of the entity.
Same column name is used for relation FK to FeatureSubtype.
Use other name for discriminator or don't use discriminator at all.
In Hibernate, discriminator column on joined inheritance is supported but not required. Hibernate is querying all subtables to determine correct subclass.
Or use for example:
th:text="${OBJECTNAME.class.getSimpleName()}"
This is far simple the using #DiscriminatorColumn...
How to mix single table type inheritance with join table type in the same inheritance tree?
I'm not using hibernate just JPA.
I know there is no official support for mixed inheritance by reading the JPA spec.
I can't change the architecture. It did worked with hibernate but now I need to implement this using openjpa.
I'm looking for some workaround.
this is working for me:
Superclass:
#Entity
#Inheritance(strategy=InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "TYPE_DISCRIMINATOR")
public class A extends SomeClass implements SomeInteface {
…
#Id
#Column(name = "ID", nullable = false, precision = 0)
public Integer getPk() {
return super.getPk();
}
…
Notice that "SomeClass" is not an entity.
Subclass - "JOIN" inheritance:
#Entity
#SecondaryTable(name = "A_SECOND_TABLE", pkJoinColumns = #PrimaryKeyJoinColumn(name ="ID") )
#DiscriminatorValue("BD")
public class B extends A implements SomeIntefaceB {
…
Create a new table "A_SECOND_TABLE" with join on super class Primary Key "ID".
each field that is not in join column and appears in our table is marked like this:
#Basic
#Column(table = "A_SECOND_TABLE", name = "STATUS", nullable = false, precision = 0)
Notice the table value.
Subclass – single table inheritance:
#Entity
public class C extends A implements SomeIntefaceC {...
simple single table inheritance.
I am trying to map a bi-directional one-to-many relationship. I am having some trouble as the "many" side references an abstract superclass. While searching the internet for possible causes I discovered that this is a known problem but I wasn't able to find a solution for my case.
I have checked the workarounds on this blog and the "Single table, without mappedBy" looks like a solution but I really need the bi-directional association.
These are the classes I am trying to map:
Owning Side
#Entity(name = "CC_Incl_Site")
public class IncludedSite {
#OneToMany(fetch=FetchType.LAZY, mappedBy = "includedSite")
private Set<CtaContractBase> ctas = new HashSet<CtaContractBase>();
#OneToMany(fetch=FetchType.LAZY, mappedBy = "includedSite")
private Set<WoContractBase> wos = new HashSet<WoContractBase>();
}
Other Side:
#Entity
public abstract class SCContract extends Contract {
#ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
#JoinColumn(name = "incl_site_id")
private IncludedSite includedSite;
}
Contract (the superclass of SCContract):
#Entity(name = "CC_CONTRACT")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "contractType", discriminatorType = DiscriminatorType.STRING)
#ForceDiscriminator
public abstract class Contract {
...
}
When trying to run the application I get this exception:
mappedBy reference an unknown target entity property:
CtaContractBase.includedSite in IncludedSite.ctas
Another solution appears to be replacing the #Entity annotation in SCContract with #MappedSuperClass but this results in another exception (Use of #OneToMany or #ManyToMany targeting an unmapped class: StudyContract.contracts[SCContract]) because in another class (StudyContract) I have
#OneToMany(fetch = FetchType.LAZY, mappedBy = "studyContract", targetEntity = SCContract.class)
#BatchSize(size = 10)
private Set<SCContract> contracts;
and as the blog explains having a collection of the superclass is not possible anymore using this approach.
Are there any other workarounds or am I missing something?
The association in IncludedSite is defined as
#OneToMany(fetch=FetchType.LAZY, mappedBy = "includedSite")
private Set<CtaContractBase> ctas = new HashSet<CtaContractBase>();
So Hibernate looks for an attribute of type IncludedSite named includedSite in the class CtaContractBase. There is no such field. The field only exists in the subclass SCContract. This means that only SCContract instances can be the target of this association, and the association should thus be defined as
#OneToMany(fetch=FetchType.LAZY, mappedBy = "includedSite")
private Set<SCContract> ctas = new HashSet<SCContract>();