Hibernate composite key and overlapping field - how to avoid column duplication - java

I am facing a problem about how to manage mapping for a specific model.
This is a multitenant application, and we have made the choice of including the "tenant_id" in every entity, so we don't have to make a joint everytime we need to get an entity (in fact, this is the root of my problem...).
The model is as follow :
+--------------------+ +---------------+
| Book | | Author |
+--------------------+ +---------------+
| id (pk) | | id (pk) |
| tenant_id (pk)(fk) | | tenant_id (pk |
| author_id (fk) | | name |
| title | +---------------+
+--------------------+
As you can see, the tenant-id is in each entity, and part of the primary key. We use #IdClass to manage the composite key. Here is the code :
#Data
public class TenantAwareKey implements Serializable {
private UUID id;
private Integer tenantId;
}
#IdClass(TenantAwareKey.class)
#Entity
#Table(name = "BOOK")
#Data
public class Book {
#Id
#GeneratedValue
#Column(name = "ID")
private UUID id;
#Id
#Column(name = "TENANT_ID")
private Integer tenantId;
private String title;
#ManyToOne
#JoinColumns(
value = {
#JoinColumn(referencedColumnName = "id", name = "author_id"),
#JoinColumn(referencedColumnName = "tenant_id", name = "tenant_id", insertable = false, updatable = false)
})
private Author author;
}
#IdClass(TenantAwareKey.class)
#Entity
#Data
public class Author {
#Id
#GeneratedValue
#Column(name = TenantAwareConstant.ENTITY_ID_COLUMN_NAME)
private UUID id;
#Id
#Column(name = TenantAwareConstant.TENANT_ID_COLUMN_NAME)
private Integer tenantId;
private String name;
}
And then, when running my application I ended up with :
Caused by: org.hibernate.AnnotationException: Mixing insertable and non insertable columns in a property is not allowed:
com.pharmagest.durnal.tenant.entity.BookNoDuplicateColumn.author
at org.hibernate.cfg.Ejb3Column.checkPropertyConsistency(Ejb3Column.java:725)
at org.hibernate.cfg.AnnotationBinder.bindManyToOne(AnnotationBinder.java:3084)
(...)
I manage to make it work if I don't try to "mutualize" the tenant_id column, it can be acceptable when I have only one foreign key with this tenant_id, but less and less as the number of foreign key increase, resulting in adding a tenant_id column each time, duplicating information et spoiling memory...
After digging a bit, I found an open issue in Hibernate : https://hibernate.atlassian.net/browse/HHH-6221
It has not been fixed for years... So, my question is : Have you faced a mapping like this, and is there a solution to avoid duplicated columen when I have a foreign-key that share a field with the primary key?

As described here, you can bypass the validation by using #JoinColumnOrFormula for the column id_tenant.
You should map the author's association like this:
#JoinColumnsOrFormulas(
value = {
#JoinColumnOrFormula(column = #JoinColumn(referencedColumnName = "id", name = "author_id")),
#JoinColumnOrFormula(formula = #JoinFormula(referencedColumnName = "tenant_id", value = "tenant_id"))
})

Related

Getting not-null constraint violation in hibernate when trying to save an entity with a joined column in Spring

I am receiving a not-null constraint violation when trying to save an entity and can't figure out exactly what my annotations and class definitions need to be to make this work.
My database consists of movies that have a one-to-many relationship with ratings. The tables and classes are defined as follows:
Movie:
Column | Type | Collation | Nullable | Default
-------------+------------------------+-----------+----------+---------
movie_id | character varying(15) | | not null |
title | character varying(250) | | not null |
year | integer | | |
runningtime | integer | | |
Indexes:
"movie_pkey" PRIMARY KEY, btree (movie_id)
Referenced by:
TABLE "rating" CONSTRAINT "rating_movie_id_fkey" FOREIGN KEY (movie_id) REFERENCES movie(movie_id)
#Entity
#Table(name = "movie")
public class Movie {
#Id
#Column(name = "movie_id")
private String id;
private String title;
private Integer year;
#Column(name = "runningtime")
private Integer runningTime;
#OneToMany(mappedBy="movie", cascade=CascadeType.ALL)
private List<Rating> ratings;
public Movie(String id, String title, Integer year, Integer runningTime) {
this.id = id;
this.title = title;
this.year = year;
this.runningTime = runningTime;
}
public Movie() {
}
Rating:
Column | Type | Collation | Nullable | Default
------------+-----------------------------+-----------+----------+-------------------------------------------
rating_id | integer | | not null | nextval('rating_rating_id_seq'::regclass)
movie_id | character varying(15) | | not null |
rating | integer | | not null |
created_at | timestamp without time zone | | |
Indexes:
"rating_pkey" PRIMARY KEY, btree (rating_id)
Foreign-key constraints:
"rating_movie_id_fkey" FOREIGN KEY (movie_id) REFERENCES movie(movie_id)
#Entity(name = "Rating")
#Table(name = "rating")
public class Rating {
#Id
#GeneratedValue(
strategy = GenerationType.IDENTITY
)
#Column(name = "rating_id")
private Long id;
#Column(name = "rating")
private Double ratingValue;
#Column(name = "created_at", insertable = false)
private LocalDateTime time;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "movie_id")
private Movie movie;
public Rating(String movieId, Double ratingValue) {
this.ratingValue = ratingValue;
this.time = LocalDateTime.now();
}
public Rating() {
}
My RatingController class has the following mapping to save a rating:
#PostMapping
public void addRating(#RequestBody Rating rating) {
ratingRepository.save(rating);
}
When my PostMapping triggers, the JSON body received consists of:
rating: "Rating{id=null, movieId=0119217, ratingValue=9.0, time=null}
But hibernate is telling me: Failing row contains (4, null, 9, null).
I can see the rating id gets generated before the save happens, as it does have a value of 4 in the error thrown, and the "time" column is handled with a timestamp generation upon creation of the entity in Postgres. However, I can't figure out how to assign a value to movie_id. I had previously kept a private String within Rating, but I realized that was redundant with the join of the columns when the only way to get it to compile was to add updateable=false, insertable=false to the movieId column annotation, at which point there was really no reason for it to be there because those annotations would cause spring to not update that value upon the save.
Does anybody know how to configure this so I can save a new rating?
As svn suggested, I had to set the Movie field before I was able to save, so I changed the param for my PostMapping to #RequestBody Map<String, String> jsonBody, called movieRepository.findById(jsonBody.get("movieId") and set the returned Movie field on the rating before saving it. If anyone knows of a more efficient method than this, please let me know, but this works for now!

Annotation Exception: mappedBy reference an unknown target entity property

I am new to Hibernate and JPA and am trying to wire up a database of movies, raters, and ratings. I keep getting the error Caused by: org.hibernate.AnnotationException: mappedBy reference an unknown target entity property: com.stephenalexander.projects.movierecommender.rating.Rating.movie in com.stephenalexander.projects.movierecommender.movie.Movie.ratings. Each Movie object and each Rater object has a OneToMany relationship with Rating. Here is how I've written things:
Rating:
package com.stephenalexander.projects.movierecommender.rating;
import com.stephenalexander.projects.movierecommender.movie.Movie;
import com.stephenalexander.projects.movierecommender.rater.Rater;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.Objects;
#Entity
#Table(name="rating")
public class Rating {
#Column(name = "rating_id")
private Long ratingId;
#Column(name = "rating")
private Double ratingValue;
private LocalDateTime time;
#ManyToOne
#JoinColumn(name = "rater_id", nullable = false)
private Rater rater;
#ManyToOne
#JoinColumn(name = "movie_id", nullable = false)
private Movie movie;
Movie:
package com.stephenalexander.projects.movierecommender.movie;
import com.stephenalexander.projects.movierecommender.rating.Rating;
import javax.persistence.*;
import java.util.List;
#Entity
#Table(name = "movie")
public class Movie {
#Id
#GeneratedValue(
strategy = GenerationType.AUTO
)
#Column(name = "movie_id")
private Integer movieId;
private String title;
private int year;
#Column(name = "posterurl")
private String posterUrl;
#Column(name = "runningtime")
private int runningTime;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "movie")
List<Rating> ratings;
Rater:
package com.stephenalexander.projects.movierecommender.rater;
import com.stephenalexander.projects.movierecommender.rating.Rating;
import javax.persistence.*;
import java.util.List;
import java.util.Objects;
#Entity
#Table(name = "rater")
public class Rater {
#Id
#GeneratedValue(
strategy = GenerationType.AUTO
)
#Column(name = "rater_id")
private Long raterId;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "rater")
private List<Rating> ratings;
I have looked at a number of other posts, and this error seems to most commonly be caused by the mappedBy annotation referencing something other than the name of the instance variable within the owner class. I believe I've done that correctly since the Rating class has a variable named rater and a variable named movie. I'm not sure what else could be causing the error. Thanks very much for reading!
EDIT: Adding my comment from an answer here to clarify how I've thought these annotations work:
As I understand it, List<Rating> ratings is mappedBy: "movie", so the movie object in Rating by its annotation will tell you what column the relationship is built on, which is the primary key of movie and foreign key of rating, movie_id. Same thing with rater_id. Is this not how the whole thing works?
EDIT 2: Another thought I have that might be a misunderstanding... are these notations not necessary if I've set up the relationships in my psql database already? Is this redundant? I thought the point of these was to essentially allow me to navigate the queries in java more easily. I was led to these notations defining relationships after I kept trying to write queries and ran into errors leading me to believe my code had no idea these tables were related. Here is how they're all set up in PostgreSQL:
-----------+-----------------------------+-----------+----------+-------------------------------------------
rating_id | integer | | not null | nextval('rating_rating_id_seq'::regclass)
rater_id | integer | | not null |
movie_id | integer | | not null |
rating | smallint | | |
time | timestamp without time zone | | |
Indexes:
"rating_pkey" PRIMARY KEY, btree (rating_id)
Foreign-key constraints:
"rating_movie_id_fkey" FOREIGN KEY (movie_id) REFERENCES movie(movie_id) ON UPDATE CASCADE
"rating_rater_id_fkey" FOREIGN KEY (rater_id) REFERENCES rater(rater_id) ON UPDATE CASCADE
Table "public.rater"
Column | Type | Collation | Nullable | Default
----------+---------+-----------+----------+---------
rater_id | integer | | not null |
Indexes:
"rater_pkey" PRIMARY KEY, btree (rater_id)
Referenced by:
TABLE "rating" CONSTRAINT "rating_rater_id_fkey" FOREIGN KEY (rater_id) REFERENCES rater(rater_id) ON UPDATE CASCADE
Table "public.movie"
Column | Type | Collation | Nullable | Default
-------------+------------------------+-----------+----------+---------
movie_id | integer | | not null |
title | character varying(100) | | not null |
year | smallint | | not null |
runningtime | smallint | | not null |
posterurl | character varying(350) | | |
Indexes:
"movie_pkey" PRIMARY KEY, btree (movie_id)
Referenced by:
TABLE "movies_genres" CONSTRAINT "fk_movie" FOREIGN KEY (movie_id) REFERENCES movie(movie_id)
TABLE "movies_directors" CONSTRAINT "fk_movie" FOREIGN KEY (movie_id) REFERENCES movie(movie_id)
TABLE "movies_countries" CONSTRAINT "fk_movie" FOREIGN KEY (movie_id) REFERENCES movie(movie_id)
TABLE "rating" CONSTRAINT "rating_movie_id_fkey" FOREIGN KEY (movie_id) REFERENCES movie(movie_id) ON UPDATE CASCADE
#Column(name = "movie_id")
private Integer movieId;
#ManyToOne
#JoinColumn(name = "movie_id", nullable = false)
private Movie movie;
U have a column with the same as movie_id in Rating and also in Movie. Hence there is a conflict.
Change either one of them.
What was missing was the targetEntity = Rating.class param to #OneToMany. So the functional annotations within the different classes are as follows:
Movie
#OneToMany(fetch = FetchType.LAZY, targetEntity = Rating.class, mappedBy = "movie")
private List<Rating> ratings
Rater
#OneToMany(fetch = FetchType.LAZY, targetEntity = Rating.class, mappedBy = "rater")
private List<Rating> ratings
Rating
#ManyToOne
#JoinColumn(name = "rater_id", nullable = false)
private Rater rater;
#ManyToOne
#JoinColumn(name = "movie_id", nullable = false)
private Movie movie;
Thanks to all who took a look at this!

Bug in Hibernate or my misunderstanding of composite Id?

I'm not big guru of JPA, but this behaviour looks strange to me, please comment!
I'm trying to persist nested Objects:
each Employee contains Set of Departments
if Department #Id is declared as #Generated int everything works fine. But it is inconvenient and I try to declare as #Id two fields (employee id + department id). But I get
EJBTransactionRolledbackException: A different object with the same identifier value was already associated with the session
on both merge and persist. Is it OK?
Some details:
Departments are declared this way
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL)
#JoinColumn(name = "snils")
private Set<DepartmentRecord> departments = new HashSet<>();
the relation is unidirectional and Persons are not declared in Departments
I populate departments this way:
record.getDepartments().add(new DepartmentRecord(snils, department));
Person is created this way:
Person record = em.find(Person.class, snils);
if (record == null)
record = new Person();
record.setSnils(snils);
record.setFullname(fullname);
record.resetLastActive();
on em.persist(record) or em.merge(record) I get an Exception.
Wrapping it to try catch block doesn't help
Transaction cannot proceed: STATUS_MARKED_ROLLBACK
What I need - is just update Person and Departments. Is it possible without boilerplate code (removing cascade, doing separate departments search et c )???
My environment: CentOS7, java8, Wildfly 10.2
Persons table is :
snils | character varying(255) | not null
blocked | integer | not null
fullname | character varying(255) |
lastactive | date |
Indexes:
"hr_workers_pkey" PRIMARY KEY, btree (snils)
Referenced by:
TABLE "hr_departments" CONSTRAINT "fk49rcpg7j54eq6fpf9x2nphnpu" FOREIGN KEY (snils) REFERENCES hr_workers(snils)
Department table is :
snils | character varying(255) | not null
department | character varying(255) | not null
lastactive | date |
Indexes:
"hr_departments_pkey" PRIMARY KEY, btree (snils, department)
Foreign-key constraints:
"fk49rcpg7j54eq6fpf9x2nphnpu" FOREIGN KEY (snils) REFERENCES hr_workers(snils)
Entity Person (actually SNILSRecord):
#Entity
#Table(name = "hr_workers")
public class SNILSRecord implements Serializable {
#Id
private String snils;
private String fullname;
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL)
#JoinColumn(name = "snils")
private Set<DepartmentRecord> departments = new HashSet<>();
#Temporal(TemporalType.DATE)
private Date lastActive;
private int blocked;
//getters and setters
}
Entity Department:
#Entity
#Table(name = "hr_departments")
public class DepartmentRecord implements Serializable {
#Id
private String snils;
#Id
private String department;
#Temporal(TemporalType.DATE)
private Date lastActive;

How to join tables on non Primary Key using JPA and Hibernate

I have 3 models User, House, UserHouseMap. And I need to access a user's house through the map. Only problem is this is an old DB & I can't change the fact that I need to map User to UserHouseMap using user.name, which is a non primary key.
Hibernate keeps giving me errors saying I need to have it as the primary key or I get errors saying A JPA error occurred (Unable to build EntityManagerFactory): Unable to find column with logical name: name in org.hibernate.mapping.Table(users) and its related supertables and secondary tables
I have tried #Formula as a workaround, but that didnt work. I also tried #JoinColumnOrFormula but that didnt work either. Here is my solution with #Formula
#Expose
#ManyToOne(targetEntity = House.class)
#Formula("(select * from houses inner join user_house_map on houses.house_name = user_house_map.house_name where user_house_map.user_name=name)")
public House house;
Here was my attempt at a #JoinColumnOrFormula solution.
#Expose
#ManyToOne(targetEntity = House.class)
#JoinColumnsOrFormulas({
#JoinColumnOrFormula(formula=#JoinFormula(value="select name from users where users.id= id", referencedColumnName="name")),
#JoinColumnOrFormula(column = #JoinColumn(name= "house_name", referencedColumnName="house_name"))
})
public House house;
Here is my mapping
#Id
#GeneratedValue
#Expose
public Long id;
#Expose
#Required
#ManyToOne
#JoinTable(
name="user_house_map",
joinColumns=
#JoinColumn(unique=true,name="user_name", referencedColumnName="name"),
inverseJoinColumns=
#JoinColumn(name="house_name", referencedColumnName="house_name"))
private House house;
Here are the DB schemas
Users
Table "public.users"
Column | Type | Modifiers
-----------------------+-----------------------------+-----------------------------
name | character varying(255) |
id | integer | not null
Indexes:
"user_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"housing_fkey" FOREIGN KEY (name) REFERENCES user_house_map(user_name) DEFERRABLE INITIALLY DEFERRED
Houses
Table "public.houses"
Column | Type | Modifiers
---------------+------------------------+-----------
house_name | character varying(255) | not null
address | text |
city | text |
state | text |
zip | integer |
zip_ext | integer |
phone | text |
Indexes:
"house_pkey" PRIMARY KEY, btree (house_name)
Referenced by:
TABLE "user_house_map" CONSTRAINT "house_map_fkey" FOREIGN KEY (house_name) REFERENCES house(house_name) DEFERRABLE INITIALLY DEFERRED
UserHouseMap
Table "public.user_house_map"
Column | Type | Modifiers
-------------+------------------------+-----------
user_name | character varying(255) | not null
house_name | character varying(255) | not null
Indexes:
"user_house_map_pkey" PRIMARY KEY, btree (user_name)
"user_house_map_house_key" btree (house_name)
Foreign-key constraints:
"user_house_map_house_fkey" FOREIGN KEY (house_name) REFERENCES houses(house_name) DEFERRABLE INITIALLY DEFERRED
Referenced by:
TABLE "users" CONSTRAINT "housing_fkey" FOREIGN KEY (name) REFERENCES user_house_map(user_name) DEFERRABLE INITIALLY DEFERRED
This is how your mapping should look like:
#Entity
public class User {
#Id
private Long id;
private String name;
#OneToMany(mappedBy = "user")
private List<UserHouseMap> houses = new ArrayList<>();
}
#Entity
public class House {
#Id
#Column(name = "house_name", nullable = false, unique = true)
private String house_name;
private String address;
#OneToMany(mappedBy = "house")
private List<UserHouseMap> users = new ArrayList<>();
}
#Entity
public class UserHouseMap implements Serializable {
#Id #ManyToOne
#JoinColumn(name = "user_name", referencedColumnName = "name")
private User user;
#Id #ManyToOne
#JoinColumn(name = "house_name", referencedColumnName = "house_name")
private House house;
}
Both User and House have access to their associated UserHouseMap entities, matching the database schema.

Optional ManyToOne with hibernate

I am trying to create and Open Source library with JavBeans + Hibernate mappings that uses a provided database to read values (it only reads, no writes). Sadly, this database is not very well designed.
My problem is with a ManyToOne relationship that is optional - i.E. it can be null.
Here are the two tables (first is types, second is metaTypes):
+--------+---------------+
| typeID | typeName |
+--------+---------------+
| 1 | Chair |
| 2 | Table |
| 3 | Picnic Table |
| 4 | Bedside Table |
+--------+---------------+
+--------+--------------+
| typeID | parentTypeID |
+--------+--------------+
| 3 | 2 |
| 4 | 2 |
+--------+--------------+
So, now there is the problem how this all belongs together. In the types table there are all kinds of types, like a list of things thet can exist.
In the second table those things are grouped together. As you can see, the "picnic table" has an entry in the metaTypes table as being a child of the type table.
If the type is a base type, there is no corresponding entry in the metaTypes table. In a well designed database, one would at least except there be exist an entry with NULL as parentTypeID, but it doesn't. I solved this by using #NotFound(action = NotFoundAction.IGNORE) in the Type bean.
In the real database, there are more columns in the metaType table containing additional, relevant information, therefore I must have a MetaType bean.
After this lengthy introduction the real question occurs:
How do I map a Type to it's variations (MetaType)?
This is what I tried (shortened, getters & setters mostly ommited):
#Entity
#Table(name = "types")
public class Type {
#Id
private int typeID;
private String typeName, description;
// meta types/ variations
#OneToOne
#JoinColumn(name = "typeID", insertable = false, updatable = false, nullable = true)
#NotFound(action = NotFoundAction.IGNORE)
private MetaType metaType; // -> this works as expected
#OneToMany
#JoinColumn(name = "typeID", referencedColumnName = "parentTypeID")
#NotFound(action = NotFoundAction.IGNORE)
private List<MetaType> variations; // this doesn't work
/**
* This method is quite easy to explain: Either this type is a base
* type, in which case it has no metaType but holds the variations directly,
* or it is a dervied type, in which case we get it's parent type and list the
* variations of the parent type.
*/
public List<MetaType> getVariations () {
if (metaType != null) {
return metaType.getParentType().getVariations();
}
return variations;
}
}
#Entity
#Table (name = "metaTypes")
public class MetaType {
#Id
private int typeID;
private Integer parentTypeID;
// id <> object associations
#OneToOne (mappedBy = "metaType")
#JoinColumn(name = "typeID", insertable = false, updatable = false)
private Type type;
#OneToOne
#JoinColumn(name = "parentTypeID", referencedColumnName = "typeID", insertable = false, updatable = false)
private Type parentType;
}
The goal should be clear.
Let's say I query for the "Bedside Table". then metaType is not null but the variations list should be.
Let's say I queried for the "Table" type. In that case there is no MetaType and thanks to the #NotFound annotation, it silently fails and there is a null in the metaType field. So far, so good. But since "Table" has a typeID of 2 I would expect the variations list to include two entries. But instead I get the exception below.
But somehow this doesn't work, I get the below exception:
Exception in thread "main" java.lang.NullPointerException
at org.hibernate.cfg.annotations.CollectionBinder.bindCollectionSecondPass(CollectionBinder.java:1456)
at org.hibernate.cfg.annotations.CollectionBinder.bindOneToManySecondPass(CollectionBinder.java:864)
at org.hibernate.cfg.annotations.CollectionBinder.bindStarToManySecondPass(CollectionBinder.java:779)
at org.hibernate.cfg.annotations.CollectionBinder$1.secondPass(CollectionBinder.java:728)
at org.hibernate.cfg.CollectionSecondPass.doSecondPass(CollectionSecondPass.java:70)
at org.hibernate.cfg.Configuration.originalSecondPassCompile(Configuration.java:1695)
at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:1424)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1844)
So, what is wrong with the approach and how do I get this to work?
OK, the following is the way I did and it seems to work fine:
The Type Entity
#Entity
public class Type {
#Id
#Column(name = "typeId")
private Integer id;
#Column(name="typeName")
private String name;
#OneToMany
#JoinColumn(name="parentTypeId")
List<MetaType> metaTypes;
}
Notice that my #OneToMany is not using a mappedBy attribute. In this case I am using #JoinColumn instead. This is a unidirectional relationship.
The MetaType Entity
#Entity
public class MetaType {
#Id
private Integer id;
#MapsId
#OneToOne
#JoinColumn(name = "typeId")
private Type type;
#ManyToOne
#JoinColumn(name = "parentTypeId")
private Type parentType;
}
Now, I recover a given type, like the example you gave and I get the right data:
TypeService service = ...;
Type t = service.getTypeById(2);
System.out.println(t.getName());
for(MetaType mt : t.getMetaTypes()){
System.out.println("\t" + mt.getType().getName() + "-> " + mt.getParentType().getName());
}
This produces the output
Table
Picnic Table-> Table
Bedside Table-> Table
It also works correctly for types without variations. If you query for type 2 (chair), it will bring a Chair with empty variations collection, which I hope is what you were expecting.

Categories

Resources