Hibernate multi-module project: extend entity to add many-to-many relationship - java

I have a project that uses Hibernate and is divided into multiple modules.
I have the following modules:
device (defines entity Device)
appstore (defines entity Application)
device-appstore-integration (adds many-to-many relationship between Device and Application).
Entities look like this:
#Entity
#Table(name = "devices")
public class Device extends AbstractAuditingEntity implements Serializable
{
#NotNull
#EmbeddedId
private DeviceIdentity identity;
// ...
}
#Entity
#Table(name = "apps")
public class App extends AbstractAuditingEntity implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// ...
}
Now I want device-appstore-integration module to add many-to-many relationship between the two entities above.
I thought about adding entity DeviceWithInstalledApps to define this many2many relationship and used the following code:
#Entity
#Table(name = "devices")
public class DeviceWithInstalledApps extends Device
{
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "devices_installed_apps",
joinColumns = {/*...*/},
inverseJoinColumns = {/*...*/}
)
private Set<App> installedApps = new HashSet<>();
// ...
}
The problem is that Hibernate requires devices table to include dtype column and treats DeviceWithInstalledApps as a separate entity that inherits from Device but I don't actually want it to be a separate entity. It's still the same entity, just with many-to-many relationship added so that I can actually access this relationship, no columns are added so there is no actual need to provide dtype column, it simply doesn't make sense in this context.
Is there any other way to define many-to-many relationship in JPA/Hibernate so that I can actually implement business logic without getting into issues mentioned above?

Related

How do you map two abstract InheritanceType.TABLE_PER_CLASS entities with a OneToMany relationship?

I'm using Hibernate with Spring Boot and JPA, and have a business requirement to retrieve and combine in to a single paged response data that is stored in four different tables in the DB.
Let's call the first two tables "tblCredits", containing Credits, and "tblDebits", containing Debits. For our purposes, those two tables are IDENTICAL - same column names, same column types, same ID fields, everything. And my endpoint is supposed to be able to return a combined list of both Credits and Debits, with the ability to search/sort by any/all of the fields being returned, and with paging.
If I controlled that DB, I would simply merge the two tables in to a single table, or create a view or stored proc which did that for me, but this is a legacy DB used by other applications which I can't modify in any way, so that's not an option.
If I didn't have to sort and page, I could just create two completely independent entities, create a separate Spring Data JPA repository for each entity, query the two repositories separately, and then just combine the results in my own code. But paging the combined results especially would get very hairy, I don't want to have to implement the merged paging logic myself unless I absolutely have to. Ideally I should be able to get JPA to handle all of that for me out-of-the-box.
I have been able achieve this first step for these first two tables using an abstract class declared as an Entity with InheritanceType.TABLE_PER_CLASS, like this:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractCreditDebitEntity {
/* literally all my properties and ID and column mappings here
...
*/
}
And then two concrete classes which extend that abstract entity and simply specify the two different table mappings, have no class-specific properties or column mappings at all:
#Entity
#Table(name = "tblCredits")
public final class Credit extends AbstractCreditDebitEntity {
//Literally nothing inside this class
}
#Entity
#Table(name = "tblDebits")
public final class Debit extends AbstractCreditDebitEntity {
//Literally nothing inside this class
}
So far so good, this works great, I am able to create a Spring JPA Repository on the AbstractCreditDebitEntity entity, under the hood that generates a union query on the two tables, and I am able to get back records from both tables in a single query, with appropriate paging and sorting. (The performance issues around union queries don't concern me at the moment.)
However, where I'm getting tripped up is on the next step, when I incorporate the additional two tables. tblCredits has a one-to-many relationship to tblCreditLineItems, and tblDebits has a one-to-many relationship to tblDebitLineItems. Again, tblCreditLineItems and tblDebitLineItems are IDENTICAL tables, from our perspective - same column names, same column types, same ID fields, everything.
So I can follow the same pattern as before for those sub-entities:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractCreditDebitLineItemEntity {
/* literally all my properties and ID and column mappings here
...
*/
}
#Entity
#Table(name = "tblCreditLineItems")
public final class CreditLineItem extends AbstractCreditDebitLineItemEntity {
//Literally nothing inside this class
}
#Entity
#Table(name = "tblDebitLineItems")
public final class DebitLineItem extends AbstractCreditDebitLineItemEntity {
//Literally nothing inside this class
}
But now I need to create the mappings between the Credit/Debit entities and CreditLineItem/DebitLineItem entities. And this is where I'm struggling. Because I need to be able to filter which specific Credit/Debit entities I return based on the values of properties inside their associated CreditLineItem/DebitLineItem entities, I need a bidirectional mapping between the two entities, and I've been unable to get that working successfully.
Here's how far I've gotten so far. First the three Credit/Debit entities with the OneToMany mapping to their associated CreditLineItem/DebitLineItem entities:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractCreditDebitEntity {
/* literally all my properties and ID and column mappings here
...
*/
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(
name = "MyIdColumnName",
referencedColumnName = "MyIdColumnName"
)
public abstract List<AbstractCreditDebitLineItemEntity> getCreditDebitLineItems();
public abstract void setCreditDebitLineItems(List<AbstractCreditDebitLineItemEntity> items);
}
#Entity
#Table(name = "tblCredits")
public final class Credit extends AbstractCreditDebitEntity {
private List<CreditLineItem> creditDebitLineItems;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, targetEntity = CreditLineItem.class)
#JoinColumn(
name = "MyIdColumnName",
referencedColumnName = "MyIdColumnName"
)
#Override
public List<AbstractCreditDebitLineItemEntity> getCreditDebitLineItems() {
return Optional.ofNullable(creditDebitLineItems).stream()
.flatMap(List::stream)
.filter(value -> AbstractCreditDebitLineItemEntity.class.isAssignableFrom(value.getClass()))
.map(AbstractCreditDebitLineItemEntity.class::cast)
.collect(Collectors.toList());
}
#Override
public void setCreditDebitLineItems(List<AbstractCreditDebitLineItemEntity> items) {
creditDebitLineItems = Optional.ofNullable(items).stream()
.flatMap(List::stream)
.filter(value -> CreditLineItem.class.isAssignableFrom(value.getClass()))
.map(CreditLineItem.class::cast)
.collect(Collectors.toList());
}
}
#Entity
#Table(name = "tblDebits")
public final class Debit extends AbstractCreditDebitEntity {
private List<DebitLineItem> creditDebitLineItems;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, targetEntity = DebitLineItem.class)
#JoinColumn(
name = "MyIdColumnName",
referencedColumnName = "MyIdColumnName"
)
#Override
public List<AbstractCreditDebitLineItemEntity> getCreditDebitLineItems() {
return Optional.ofNullable(creditDebitLineItems).stream()
.flatMap(List::stream)
.filter(value -> AbstractCreditDebitLineItemEntity.class.isAssignableFrom(value.getClass()))
.map(AbstractCreditDebitLineItemEntity.class::cast)
.collect(Collectors.toList());
}
#Override
public void setCreditDebitLineItems(List<AbstractCreditDebitLineItemEntity> items) {
creditDebitLineItems = Optional.ofNullable(items).stream()
.flatMap(List::stream)
.filter(value -> DebitLineItem.class.isAssignableFrom(value.getClass()))
.map(DebitLineItem.class::cast)
.collect(Collectors.toList());
}
}
And then the three CreditLineItem/DebitLineItem entities with their ManyToOne mappings back to the Credit/Debit entities:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractCreditDebitLineItemEntity {
/* literally all my properties and ID and column mappings here
...
*/
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(
name = "MyIdColumnName",
referencedColumnName = "MyIdColumnName",
updatable = false,
insertable = false)
public abstract AbstractCreditDebitEntity getCreditDebit();
public abstract void setCreditDebit(AbstractCreditDebitEntity creditDebitEntity);
}
#Entity
#Table(name = "tblCreditLineItems")
public final class CreditLineItem extends AbstractCreditDebitLineItemEntity {
private Credit creditDebit;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(
name = "MyIdColumnName",
referencedColumnName = "MyIdColumnName",
updatable = false,
insertable = false)
#Override
public Credit getCreditDebit() {
return creditDebit;
}
#Override
public void setCreditDebit(AbstractCreditDebitEntity creditDebitEntity) {
creditDebit =
Optional.ofNullable(creditDebitEntity)
.filter(value -> Credit.class.isAssignableFrom(value.getClass()))
.map(Credit.class::cast)
.orElse(throw new RuntimeException());
}
}
#Entity
#Table(name = "tblDebitLineItems")
public final class DebitLineItem extends AbstractCreditDebitLineItemEntity {
private Debit creditDebit;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(
name = "MyIdColumnName",
referencedColumnName = "MyIdColumnName",
updatable = false,
insertable = false)
#Override
public Debit getCreditDebit() {
return creditDebit;
}
#Override
public void setCreditDebit(AbstractCreditDebitEntity creditDebitEntity) {
creditDebit =
Optional.ofNullable(creditDebitEntity)
.filter(value -> Debit.class.isAssignableFrom(value.getClass()))
.map(Debit.class::cast)
.orElse(throw new RuntimeException());
}
}
This code compiles, however... when in my automated tests I try to persist one of my Credit entities (I use a simple H2 database for my automated tests), I get the following error:
2021-04-02 13:53:52 [main] DEBUG org.hibernate.SQL T: S: - update AbstractCreditDebitLineItemEntity set MyIdColumnName=? where ID=?
2021-04-02 13:53:52 [main] DEBUG o.h.e.jdbc.spi.SqlExceptionHelper T: S: - could not prepare statement [update AbstractCreditDebitLineItemEntity set MyIdColumnName=? where ID=?]
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "ABSTRACTCREDITDEBITLINEITEMENTITY" does not exist
It appears to be trying to persist based on the #OneToMany mapping from my AbstractCreditDebitEntity class to my AbstractCreditDebitLineItemEntity. Which, since it's an abstract class with InheritanceType.TABLE_PER_CLASS, has no table specified for it, so it assumes the table it needs to persist to has the same name as the class.
What I wanted to happen here is for the #OneToMany mapping on the concrete getter in the Credit subclass, which specifies its targetEntity as the concrete CreditLineItem.class, to essentially override/replace the #OneToMany mapping on its parent abstract class. But it seems the mapping on the concrete class gets completely ignored?
I could remove the #OneToMany mapping from the AbstractCreditDebitEntity class entirely, and only define that mapping in the two concrete Credit/Debit entities that extend it. That makes the persistence error go away, and 90% of my test cases pass... but in that case when I try to filter or sort the results coming back from the combined AbstractCreditDebitEntity Spring Data JPA repository based on one of the fields that only exists in the CreditLineItem/DebitLineItem sub-entity, the query fails due to the AbstractCreditDebitEntity no longer having any mapping to the AbstractCreditDebitLineItemEntity.
Is there any good way of resolving this problem, so that the OneToMany mapping from AbstractCreditDebitEntity to AbstractCreditDebitLineItemEntity still exists, but the knowledge that the Credit entity maps specifically to the CreditLineItem entity and the Debit entity maps specifically to the DebitLineItem entity is also maintained?
After a lot of experimentation, I found something that works for me.
Basically, rather than try to override the OneToMany mapping in the abstract entity class with the OneToMany mappings in the concrete entities, I had to make them completely separate mappings to completely different properties. Which means my concrete entities have two different collections of AbstractCreditDebitLineItemEntity, and some AbstractCreditDebitLineItemEntity objects will appear twice, in both collections. A bit wasteful in terms of memory/computation, but I'm okay with that, it works!
So here's what I ended up with:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractCreditDebitEntity {
/* literally all my properties and ID and column mappings here
...
*/
private List<AbstractCreditDebitLineItemEntity> creditDebitLineItems;
#OneToMany(fetch = FetchType.LAZY, targetEntity = AbstractCreditDebitLineItemEntity.class)
#JoinColumn(
name = "MyIdColumnName",
referencedColumnName = "MyIdColumnName",
updatable = false,
insertable = false
)
public List<AbstractCreditDebitLineItemEntity> getCreditDebitLineItems() {
return creditDebitLineItems;
}
public void setCreditDebitLineItems(List<AbstractCreditDebitLineItemEntity> items) {
creditDebitLineItems = items;
}
}
#Entity
#Table(name = "tblCredits")
public final class Credit extends AbstractCreditDebitEntity {
private List<CreditLineItem> creditLineItems;
#OneToMany(cascade = CascadeType.ALL, targetEntity = CreditLineItem.class)
#LazyCollection(LazyCollectionOption.FALSE)
#JoinColumn(
name = "MyIdColumnName",
referencedColumnName = "MyIdColumnName"
)
public List<CreditLineItem> getCreditLineItems() {
return creditLineItems;
}
#Override
public void setCreditDebitLineItems(List<CreditLineItem> items) {
creditLineItems = items;
}
}
With the exact same pattern repeated for the Debit entity.
This allows me to both:
persist, using the OneToMany mappings from the concrete Credit and Debit entities to the concrete CreditLineItem and DebitLineItem entities; and
do finds on the Spring Data JPA repository of AbstractCreditDebitEntity, using the the completely separate OneToMany mapping from that abstract entity to the AbstractCreditDebitLineItemEntity.
Not as clean as if I'd been able to override the OneToMany mapping in the abstract parent class with a more specific OneToMany mapping in the concrete child classes... but as I said, it works!
(The answer on this issue helped me know I needed to replace fetchType=FetchType.EAGER on my concrete OneToMany mappings with #LazyCollection(LazyCollectionOption.FALSE):
Hibernate throws MultipleBagFetchException - cannot simultaneously fetch multiple bags)

Spring data JPA repository returns entity of parent class

I have strange problem with spring data and inheritance, i have two classes:
#Getter
#Setter
#Entity
#Table(name = "a")
#Inheritance(strategy = InheritanceType.JOINED)
public class A {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "a_id_gen")
#SequenceGenerator(name = "a_id_gen", sequenceName = "a_id_seq", allocationSize = 50)
#Column(name = "id")
private Long id;
}
And class B
#Getter
#Setter
#Entity
#Table(name = "b")
public class B extends A {
#ManyToOne
#JoinColumn(name = "subject")
private Subject subject;
}
Also i have two simple interfaces which extends JpaRepo like this:
public interface ARepository extends JpaRepository<A, Long>
public interface BRepository extends JpaRepository<B, Long>
And then in code in #Transactional i use it like this:
A a = ARepository.findOne(someId);
if (some checks here) {
B b = BRepository.findOne(a.getId());
}
And a problem that B here is NULL, however in DB in table b it exists with same ID 100% sure. IF in debug i write
BRepository.getOne(a.getId());
it returns instance of A, same instance A as above from ARepository.
How i could make this work as i need? I think that problem in some hibernate managed cache or something. I also tried to change equals and hashcode like in this example http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#identifiers-composite-associations but no luck, problem still there.
Hibernate version is: 5.0.12.Final
Spring boot dependencies: 1.5.6.RELEASE
Ok i found out problem cause. It was query earlier in transaction. JOOK was used to create recursive sql request, and hibernate to map this request to entity. Because of entity have inheritance for mapping i have to add "clazz_" field in request with hard coded 0, after this request all entity was cached in first lvl hibernate cache somehow and cant be then reRequested from DB.
I add to my JOOK
.select(when(B.ID.isNotNull(), 1).otherwise(0).as("clazz_"))
And now all working as expected

Spring Data Repository for Inheritance.JOINED subclass with PrimaryKeyJoinColumn and Composite Key in Super

I have two entities that map to two DB tables. One inherits from the other. The subclass also shares a composite foreign key with the parent.
Foo Entity
#Entity
#Table(name = "foo")
#IdClass(FooKey.class)
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn(name = "discriminator_flag", discriminatorType = DiscriminatorType.CHAR)
#DiscriminatorValue("F")
#Customizer(FooCustomizer.class)
public class Foo {
#Id
#Column(name = "id_col_1")
private String idCol1;
#Id
#Column(name = "id_col_2")
private String idCol2;
...
}
Bar Entity
#Entity
#Table(name = "bar")
#DiscriminatorValue("B")
#PrimaryKeyJoinColumns({ #PrimaryKeyJoinColumn(name = "foo_id_col_1", referencedColumnName = "id_col_1"), #PrimaryKeyJoinColumn(name = "foo_id_col_2", referencedColumnName = "id_col_2") })
public class Bar extends Foo {
...
}
Foo Repository
public interface FooRepository extends CrudRepository<Foo, FooKey> {
}
Bar Repository
public interface BarRepository extends CrudRepository<Bar, FooKey> {
}
The FooRepository works like a charm. No problems. But the BarRepository causes an exception in EclipseLink 2.5.2 (Spring-data 1.7.2.RELEASE)
No #IdClass attributes exist on the IdentifiableType [EntityTypeImpl#28997437:Bar [ javaType: class com.example.Bar descriptor: RelationalDescriptor(com.example.Bar --> [DatabaseTable(foo), DatabaseTable(bar)]), mappings: 49]]. There still may be one or more #Id or an #EmbeddedId on type.
I tried adding an #IdClass(FooKey.class) annotation to Bar but it complains that it can't see the matching #Id fields on the entity (of course they are on the super class)
Anyone have any ideas how to implement a BarRepository successfully for this?
UPDATE
I believe this to be a problem with EclipseLink and its handling of composite keys in an inheritance structure. I tried doing this with a MappedSuperclass and moved the key and common items out of Foo into AbstractFoo and found that both concrete implementations of it (Foo and Bar) both threw the error. Adding in the #IdClass annotation onto both as a workaround proved just as useless.
What did work was removing the composite key and replacing it with a simple single property #Id. Everything just worked normally.
So faced with the option of refactoring everything to use a single key and then enforcing a unique constraint on my two composite key columns at the database level or removing the inheritance and applying a #OneToOne mapping from Bar to Foo with #PrimaryKeyJoinColumns and cascading all operations, I went with the cop-out option 2. So my Bar entity now looks like
Bar Entity
#Entity
#Table(name = "bar")
#IdClass(BarKey.class)
public class Bar extends Foo {
#Id
#Column(name = "foo_id_col_1")
private String fooIdCol1;
#Id
#Column(name = "foo_id_col_2")
private String fooIdCol2;
#OneToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#PrimaryKeyJoinColumns({ #PrimaryKeyJoinColumn(name = "foo_id_col_1", referencedColumnName = "id_col_1"), #PrimaryKeyJoinColumn(name = "foo_id_col_2", referencedColumnName = "id_col_2") })
Private Foo foo;
...
}
Now my BarRepository works fine with the caveat that I have to create a Foo entity and attach it to a Bar entity before I persist. For me this was far, far less work.

JPA ManyToMany with own Table

I have the following entities:
Project: id:int, name:String, workers:Set
Worker: id:int, name:String, projects:Set
Project_Worker: id:int,
project_id:int, worker_id:int
So, I have my own table to 'solve' the many to many relation. <-- ya I need it - can't use just many to many and let JPA create the table cause I need the extra entity.
Now I tried a lot. Just so that JPA wont create an own table and extra columns and so on, but it wont work. My code so far:
#Entity
#Table("Worker")
public class Worker implements Serializable {
...
#ManyToMany
#JoinTable(name = "Project_Worker",
joinColumns = {#JoinColumn(name = "Worker_id", referencedColumnName="id")},
inverseJoinColumns = {#JoinColumn(name = "project_id", referencedColumnName="id")})
private Set<Project> projects;
The Project entity looks the same. just with the changed join columns.
My table: Project_Worker columns have at the moment no annotations cause if I use for example:
#JoinTable(name="Project",
joinColumns = {#JoinColumn(name = "id")})
JPA creates in the worker and project table an extra column instead in the Project_Worker table..
So I just want to map the many-to-many relation over my own table with my own attributes.
I also tried the solution - which worked - that just the project_worker table will have OneToMany annotations, but so the project and worker entity won't be able to use the Set and i have to look every time in the project_worker table for the right row...
If you want to map it as a many-to-many Project<->Worker via your join table you will need to ditch the id column in the project_worker table and it should work as you expect.
If you can't do this then you will need ProjectWorker as an entity in code then the relationship should be a one-to-many from Worker and Project to ProjectWorker.
#Entity
#Table("Worker")
public class Worker implements Serializable {
...
#OneToMany(mappedBy = "worker")
private Set<ProjectWorker> projectWorker;
#Entity
#Table("Project")
public class Project implements Serializable {
...
#OneToMany(mappedBy = "project")
private Set<ProjectWorker> projectWorker;
#Entity
#Table("ProjectWorker")
public class ProjectWorker implements Serializable {
...
#ManyToOne
#JoinColumn(name = "worker_id")
private Worker worker;
#ManyToOne
#JoinColumn(name = "project_id")
private Project project;

Unidirectional OneToMany relation with JoinTable - table not generated

I tried to create an unidirectional OneToMany relation with a JoinTable in Play Framework 2.1. However, the framework is not generating the 'JoinTable': "transformation_input_files". The strange part is that if I change the relation to ManyToMany the table is generated. Here is the code:
So its about an Transformation class containing multiple S3Files. Here is the Transformation file:
#Entity
#Table(name = "transformations")
public class Transformation extends Model {
#Id
public Long id;
/*...*/
#OneToMany(cascade = CascadeType.PERSIST)
#JoinTable(
name="transformation_input_files",
joinColumns = #JoinColumn( name="transformation_id"),
inverseJoinColumns = #JoinColumn( name="input_file_id")
)
public List<S3File> inputFiles;
}
Here is the S3File:
#Entity
#Table(name="s3files")
public class S3File extends Model {
#Id
public Long id;
/*...*/
}
The S3Files are used in more models, so that can not be a bidirectional relation. If I change #OneToMany in #ManyToMany it does generate the join table, however, I do like to stick with the #OneToMany relation.
How do I solve this problem? Did I missed something?
In one-to-many relationship you have to store the relation in "Many" entity..if you don't want to store the value in the S3File class you have to create another class to join the two classes.
Example:
#Entity
#Table(name="transformations_ s3files")
public class Relation extends Model {
#Id
public Long id;
#ManyToOne
public Transformation transformation;
#OneToOne
public S3File file;
}

Categories

Resources