JPA: Multiple many-to-many relations between two entities? - java

I have two entity classes 'User' and 'Document'. Each user has an inbox and an outbox which are in fact two List and each Document may reside in multiple inbox's and outbox's of users. Here are my classes:
#Entity
public class User {
#Id
private Long id;
#ManyToMany(mappedBy = "userinbox", cascade=CascadeType.ALL)
private List<Document> inbox = new ArrayList<Document>();
#ManyToMany(mappedBy = "useroutbox", cascade=CascadeType.ALL)
private List<Document> outbox = new ArrayList<Document>();
}
#Entity
public class Document {
#Id
private Long id;
#ManyToMany(cascade=CascadeType.ALL)
private List<User> userinbox = new ArrayList<User>();
#ManyToMany(cascade=CascadeType.ALL)
private List<User> useroutbox = new ArrayList<User>();
}
When I run the programm and try to assign a document to a user's inbox (and vice-versa), I get the following error:
Error Code: 1364
Call: INSERT INTO DOCUMENT_USER (userinbox_ID, inbox_ID) VALUES (?, ?)
bind => [2 parameters bound]
Internal Exception: java.sql.SQLException: Field 'useroutbox_ID' doesn't have a default value
Query: DataModifyQuery(name="userinbox" sql="INSERT INTO DOCUMENT_USER (userinbox_ID, inbox_ID) VALUES (?, ?)")
The generated association table looks like this:
DOCUMENT_USER
useroutbox_ID | outbox_ID |userinbox_ID | inbox_ID
How would I assign default values for such a many-to-many relation? Would it be better to make two association tables -> one for inbox-relation, another for outbox-relation? How would I accomplish that ? Other solutions to this problem ?
Any help highly appreciated - many thanks in advance!

I think that the better option is to have two separate tables, one per relation. Because you actually have two relations between two different entities, and not one relation with four different entities.
So, you should add a #JoinTable annotation to each of your attributes in the Document side since the User side those relations are mapped to a property. Something like the following:
#Entity
public class Document {
#Id
private Long id;
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(name = "document_inbox", joinColumns = #JoinColumn(name = "userinbox_id"),
inverseJoinColumns = #JoinColumn(name = "inbox_id"))
private List<User> userinbox = new ArrayList<User>();
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(name = "document_outbox", joinColumns = #JoinColumn(name = "useroutbox_id"),
inverseJoinColumns = #JoinColumn(name = "outbox_id"))
private List<User> useroutbox = new ArrayList<User>();
}
Leave the other entity as it is now. Hope this helps.

Related

Hibernate and Criteria Api generates wrong Join condition

I got following tables. Lets ignore the fact that the relation is done wrong here. I cannot change that.
Each company can have multiple employes and each employe belongs to only one company.
Table: Company
ID
EMPLOYE_ID
10
100
Table: Employe
ID
NAME
100 (Same as EMPLOYE_ID)
John
Now i want to create a relation #OneToMany between Company -> Employe . My entities look as follow
class Company {
#Id
#Column(name = "id", unique = true, nullable = false)
private String id;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "EMPLOYE_ID", referencedColumnName = "ID")
private Set<Employe> employees;
}
No matter if i try to create a uniderectional, or biderection relationship by adding also #ManyToOne on my Employe class, when using Criteria api to select all Company entities and their Employes i always end up with a wrong generated SQL query at the point where it joines the tables. The above relation for example creates following:
FROM company company0
INNER JOIN employe employe0 ON company0.id = employe0.employe_id
I tried several approaches, but i end up almost with the same error. It tries either to access a column which does not exist on the table, or joins wrong columns (e.g. id = id). Or by the following exception
Caused by: org.hibernate.MappingException: Repeated column in mapping
for entity: com.Employe column: id (should be mapped with
insert="false" update="false")"}}
What is a simple approach to create a bidrectional relation with the above table structure?
Note: I finally ended up changing the DB schema. Still, it would be interesting if someone could provide an answer for such a case, even if it is based on a not well formed
The central problem is that the described table structures do not allow a 1:n relationship from Company to Employee. According to the table design (especially the design of PKs) above, a company can only have one employee.
However, if the DB design cannot be changed, the following approach using the JoinColumnOrFormula annotation may lead to partial success.
The #JoinColumnOrFormula annotation is used to customize the join between a child Foreign Key and a parent row Primary Key when we need to take into consideration a column value as well as a #JoinFormula.
See https://docs.jboss.org/hibernate/stable/orm/userguide/html_single/Hibernate_User_Guide.html#associations-JoinColumnOrFormula for details.
More concretely with these Entities
#Entity
#Table(name="t_company")
public class Company {
#Id
#Column(name="id")
private Integer id;
#Column(name="employee_id")
private Integer employeeId;
#OneToMany(mappedBy = "company")
private List<Employee> employees;
// ..
}
#Entity
#Table(name = "t_employee")
public class Employee {
#Id
#Column(name = "id")
private Integer id;
#Column(name = "name")
private String name;
#ManyToOne
#JoinColumnOrFormula( column =
#JoinColumn(
name = "id",
referencedColumnName = "employee_id",
insertable = false,
updatable = false
)
)
private Company company;
// ..
}
and this custom repository
#Repository
public class EmployeeRepository {
#Autowired
EntityManager entityManager;
List<Employee> findAll() {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
Root<Employee> root = cq.from(Employee.class);
Join<Employee, Company> joinCompany = root.join("company");
TypedQuery<Employee> query = entityManager.createQuery(cq);
return query.getResultList();
}
}
you get the following query:
select
employee0_.id as id1_1_,
employee0_.name as name2_1_
from t_employee employee0_
inner join t_company company1_ on employee0_.id=company1_.employee

Delete hibernate entity which is referenced by #ManyToMany in other entity

I want to delete Recipe (using spring data DAO) but I got SQL exception: org.postgresql.util.PSQLException: ERROR: update or delete on table "recipe" violates foreign key constraint "fkacys689tmdmfggtf4thdoc83k" on table "favourite_recipes"
Detail: Key (id)=(76823) is still referenced from table "favourite_recipes".
My entities:
#Entity
public class Account {
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "favourite_recipes",
joinColumns = #JoinColumn(name = "account_id"),
inverseJoinColumns = #JoinColumn(name = "recipe_id"))
private Set<Recipe> favouriteRecipes = new HashSet<>(0);
...
}
#Entity
public class Recipe {
...
}
How to remove recipe instance?
You need to handle the cascade type, by default is set to ALL.
For example you can work around the contraints like this:
#ManyToMany(cascade = CascadeType.DETACH)
more info : cascade type docs
in you need to delete from the owning entity side which is the Account.
So first remove the recipe from recipe list in Account and save the account, then remove the recipe itself.
As Amer Qarabsa metioned I had to remove recipe from Account.
I added new field in Recipe to get bidirectional mapping
#ManyToMany(cascade = CascadeType.MERGE, mappedBy = "favouriteRecipes")
private Set<Account> recipeLovers = new HashSet<>(0);
Code in service class to remove recipe from all accounts + clear lovers in recipe (recipe and recipeId variables are not initialized here)
Set<Account> recipeLovers = recipe.getRecipeLovers();
recipeLovers.forEach(account ->
account.getFavouriteRecipes()
.removeIf(r -> r.getId() == recipeId));
recipeLovers.clear();
recipeDao.delete(recipe);

Hibernate persist entity without fetching association object. just by id

I have an simple association between 2 entities:
public class Car {
...
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private User user;
...
}
and
public class User {
#Id
#GeneratedValue
#Column(name = "user_id")
private long userId;
...
#OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
private Set<Car> cars;
...
}
Then I get some user id from client. For example, userId == 5;
To save car with user I need to do next:
User user = ... .findOne(userId);
Car car = new Car();
car.setUser(user);
... .save(car);
My question is: Can I persist car record without fetching user?
Similarly like I would do by using native SQL query: just insert userId like string(long) in Car table.
With 2nd lvl cache it will be faster but in my opinion I don't need to do extra movements. The main reason that I don't want to use native Query is because I have much more difficult associations in my project and I need to .save(car) multiple times. Also i don't want to manually control order of query executions.
If I use session.createSQLQuery("insert into .....values()") will the Hibernate's batch insert work fine?
Correct me if I'm wrong.
Thanks in advance!
UPDATE:
Actually the mapping is similar to:
There is #ManyToMany association between User and Car. But cross table is also an entity which is named, for example, Passanger. So the mapping is next:
public class User{
#OneToMany(fetch = FetchType.LAZY, mappedBy = "user", targetEntity = Passenger.class)
private Set<Passenger> passengers;
}
Cross entity
#IdClass(value = PassengerPK.class)
public class Passenger {
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private User user;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "car_id")
private Car car;
... other fields ...
}
Car entity:
public class Car {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "car", targetEntity = Passenger.class, cascade = CascadeType.ALL)
private Set<Passenger> passengers;
}
And the code:
List<User> users = ... .findInUserIds(userIds); // find user records where userId is IN userIds - collection with user Ids
Car car = new Car(); //initialization of car's fields is omitted
if (users != null) {
car.setPassengers(new HashSet<>(users.size()));
users.forEach((user) -> car.getPassengers().add(new Passenger(user, car)));
}
... .save(car);
"Can I persist car record without fetching user?"
Yes, that's one of the good sides of Hibernate proxies:
User user = entityManager.getReference(User.class, userId); // session.load() for native Session API
Car car = new Car();
car.setUser(user);
The key point here is to use EntityManager.getReference:
Get an instance, whose state may be lazily fetched.
Hibernate will just create the proxy based on the provided id, without fetching the entity from the database.
"If I use session.createSQLQuery("insert into .....values()") will the Hibernate's batch insert work fine?"
No, it will not. Queries are executed immediately.
If someone is using Spring Data JPA: The same can be achieved in Spring Data JPA can be done using the method
JpaRepository.getReferenceById(ID id)
This replaced the former
getOne(ID)
Hibernate users can implement this method:
public <T extends Object> T getReferenceObject(Class<T> clazz, Serializable id) {
return getCurrentSession().get(clazz, id);
}
And call like:
MyEntity myEntity = getRefererenceObject(MyEntity.class, 1);
You can change id type to Integer or Long as per your entity model.
Or T can be inherited from your BaseEntity if you have one base class for all entities.
The following approach works for me:
User user = new User();
user.setId(userId);
car.setUser(user);

Complex query for hibernate

I have three tables with entities in hibernate. DB - MySQL. I need to get fields from entity "Item" where ModelsMm.id has some value. At first I tried to do separate queries, it was huge amount of requests in sum. So, i tried to do complex query, but it became a very long run.
I think there is a simpler way, but I do not know what.
My query and entities.
List<Item> itemIds = session.createQuery("select it from Item it where :id in elements(it.mmPrice.modelsMm)");
#Entity (name = "MODELS_MM")
public class ModelsMm {
#Id
private int Id;
#ManyToMany(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
#JoinTable(name="parth_mm", joinColumns={#JoinColumn(name="MODEL_ID", referencedColumnName="ID")}, inverseJoinColumns={#JoinColumn(name="PART_ID", referencedColumnName="ID")})
private List<MmPrice> mmPrices;
#Entity (name = "MM_PRICE")
public class MmPrice {
#Id
private int id;
private String article;
#OneToOne(optional = true, fetch = FetchType.LAZY)
#JoinColumn(name = "article", referencedColumnName = "article",insertable = false, updatable = false)
private Item item;
#ManyToMany
#JoinTable(name="parth_mm", joinColumns={#JoinColumn(name="PART_ID", referencedColumnName="ID")}, inverseJoinColumns={#JoinColumn(name="MODEL_ID", referencedColumnName="ID")})
private List<ModelsMm> modelsMm;
#Entity
#Table(name="SHOP_ITEMS")
public class Item implements Serializable {
#Id
private int id;
private String article;
#OneToOne(optional = true, fetch = FetchType.LAZY)
#JoinColumn(name = "article", referencedColumnName = "article",insertable = false, updatable = false)
private MmPrice mmPrice;
In console i have that query
Hibernate: select item0_.ID as ID0_, item0_.ARTICLE as ARTICLE0_, item0_.article as article0_ from SHOP_ITEMS item0_ cross join MM_PRICE mmprice1_ where item0_.article=mmprice1_.article and (? in (select modelsmm2_.MODEL_ID from parth_mm modelsmm2_ where mmprice1_.ID=modelsmm2_.PART_ID))
Thanks.
First, you'll have to fix your mapping. In a bidirectional association, one side MUST be the inverse side, and thus use the mappedBy attribute. For example, if you choose ModelsMm to be the inverse side, then its mmPrices attribute should be declared as
#ManyToMany(mappedBy = "modelsMm")
private List<MmPrice> mmPrices;
You should also forget about CascadeType.ALL on ManyToMany associations: it makes no sense. You don't want to delete all the courses of a student when you delete a student, since the course is also followed by several other students.
Now, regarding your query, it's not very clear what you want to do. If you want to select all the items which have a price which have at least one model whose ID is in a collection of IDs, then you simply need the following query:
select distinct i from Item i
join i.mmPrice p
join p.modelsMm m
where m.id in :modelIds
Side note: please fix your naming. This inconsistent and unnecessary usage of mm as a prefix or suffix makes the code unreadable. Name your class Price, the fields of type Price price, and the collections of prices prices. Just as you would do in English: an Item has a price, and a price has models.

#ManyToMany - data does not persist in Database

Simplifying, in my database I have tables:
Car (pk="id_car")
CarAddon (pk="id_car_fk,id_addon_fk",
`FK_car_addon_addon` FOREIGN KEY (`id_addon_fk`) REFERENCES `addon` (`id_addon`)
`FK_car_addon_car` FOREIGN KEY (`id_car_fk`) REFERENCES `car` (`id_car`)
Addon (pk="id_addon")
Shortly: I have cars, many cars can has many addons (like ABS etc).
There are tables with cars, addons, and one table which is logical connection.
Overall, entities work fine. I have no problems with persist data, when I want persist single object. I don't have problems, when I want FETCH data, ie. Car->getAddon();
But, when I'm going to persisting a collection, nothing happens. No exceptions were thrown, there were no new data in database.
//DBManager is a singleton to create an EntityManager
EntityManager em = DBManager.getManager().createEntityManager();
em.getTransaction().begin();
Addon addon1 = new Addon();
addon1.setName("czesc1");
em.persist(addon1);
Addon addon2 = new Addon();
addon2.setName("czesc2");
em.persist(addon2);
car.setAddonCollection(new ArrayList<Addon>());
car.getAddonCollection().add(addon1);
car.getAddonCollection().add(addon2);
em.persist(car);
em.getTransaction().commit();
In this case, addons were stored in Addon table, car in Car table. There are no new data in CarAddon table though object car has good data (there is addon collection in debbuger).
When I changed em.persist(car) to em.merge(car) I got an exception:
"SEVERE: Persistence error in /admin/AddAuction : java.lang.IllegalStateException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: model.entity.Car[ idCar=0 ]."
Simple version of my classess:
#Entity
#Table(name = "addon")
#XmlRootElement
#NamedQueries({...})
public class Addon implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#NotNull
#Column(name = "id_addon")
private Integer idAddon;
#Size(max = 100)
#Column(name = "name")
private String name;
#JoinTable(name = "car_addon",
joinColumns = {
#JoinColumn(name = "id_addon_fk", referencedColumnName = "id_addon")},
inverseJoinColumns = {
#JoinColumn(name = "id_car_fk", referencedColumnName = "id_car")})
#ManyToMany
private List<Car> carCollection;
#XmlTransient
public List<Car> getCarCollection() {
return carCollection;
}
public void setCarCollection(List<Car> carCollection) {
this.carCollection = carCollection;
}
}
#Entity
#Table(name = "car")
#XmlRootElement
#NamedQueries({...)
public class Car implements Serializable {
#ManyToMany(mappedBy = "carCollection", fetch= FetchType.EAGER, cascade=CascadeType.ALL)
private List<Addon> addonCollection;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#NotNull
#Column(name = "id_car")
private Integer idCar;
#XmlTransient
public List<Addon> getAddonCollection() {
return addonCollection;
}
public void setAddonCollection(List<Addon> addonCollection) {
this.addonCollection = addonCollection;
}
}
How can I fix it?
ps1. I have:
cascade=CascadeType.ALL przy #ManyToMany private List<Car> carCollection
but this dos not solve my problem.
ps2. I am using Netbeans 7, EclipseLink and MySQL (not Hibernate - I have problem with it)
I have one theory that always seems to trip people up with many-to-many collections. The problem is that in memory, the associations are made in two places. Both in the car's addons list and in the addon's cars list. In the database, there isn't such a duplication.
The way JPA providers get around this is through the mappedBy attribute. Since you have mappedBy on the car's addons list this means that the relationship is actually controlled by the addon's cars list (confusing I know).
Try adding the following:
addon1.setCarCollection(new ArrayList<Car>());
addon1.getCarCollection().add(car);
addon2.setCarCollection(new ArrayList<Car>());
addon2.getCarCollection().add(car);
before you persist the car.
Generally speaking, I would avoid many-to-many associations. What you really have is an intermediate link table, with a one-to-many and a many-to-one. As soon as you add anything of interest to that link table (e.g. datestamp for when the association was made), poof, you are no longer working with a pure many-to-many. Add in the confusion around the "owner" of the association, and you're just making things a lot harder than they should be.
could you try add
#JoinTable(name = "car_addon",
joinColumns = {
#JoinColumn(name = "id_addon_fk", referencedColumnName = "id_addon")},
inverseJoinColumns = {
#JoinColumn(name = "id_car_fk", referencedColumnName = "id_car")})
to both side
just reverse the joinColumns and inverseJoinColumns
Try adding (fetch = FetchType.EAGER) to your ManyToMany annotation

Categories