I have simple 3 tables:
Users
-id
-name
Addresses
-id
-user_id //FK
-location
Phone
-id
-number
-address_id //FK
Users can have many Addresses
Addresses can have many Phone
Thus, annotations applied to each entity class would look like:
Users
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private List<Addresses> addresses;
Addresses
#ManyToOne()
#JoinColumn(name = "user_id")
private Users user;
#OneToMany(mappedBy = "address")
private List<Phone> phone;
Phone
#ManyToOne()
#JoinColumn(name = "address_id")
private Addresses address;
The annotation above should create relationship:
users (1) ----- (n) addresses (1) ------ (n) phone
Then I created some test data in MySQL and tried to run test to see if query result that returns "users" would include reference to addresses and phone.
I've tried with 2 table only using users and addresses and got result ok.
Now that I added "phone" table which is 3rd table then I'm getting error shown below.
Is this really right way to join 3 or more tables?
Test code
try {
//get entity manager
String WEB_PU = "NoJSFPU";
EntityManagerFactory factory = Persistence.createEntityManagerFactory(WEB_PU);
EntityManager em = factory.createEntityManager();
//run query
List<Users> usersList = em.createQuery("SELECT u FROM Users u").getResultList();
//loop to print out each result
for(Users item : usersList) {
//display user info
System.out.println(item.getId() + " " + item.getFirstname());
List<Addresses> addrs = item.getAddresses();
for(Addresses ad : addrs) {
//display address info
System.out.println("Address:::" + ad.getLocation());
List<Phone> pho = ad.getPhone();
for(Phone p : pho) {
//display phone info
System.out.println("phone#" + p.getNumber());
}
}//end inner for
}//end outer for
System.out.println("==========================");
}
catch(Exception e) {
System.out.println("seriously wtf: " + e.toString());
System.exit(1);
}
Error
Exception Description: predeploy for PersistenceUnit [NoJSFPU] failed.
Internal Exception: Exception [TOPLINK-7250] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.ValidationException
Exception Description: [class local.test.session.Addresses] uses a non-entity [class local.test.session.Phone] as target entity in the relationship attribute [private java.util.List local.test.session.Addresses.phone].
I figured out. Had to specify by using #JoinTable annotation.
Now If I run the test code, data from all 3 tables can be obtained :D
Addresses
#ManyToOne
#JoinTable(name = "Addresses", joinColumns = {#JoinColumn(name="user_id",
referencedColumnName = "user_id") } )
private Users user;
Phone
#ManyToOne
#JoinTable(name = "Phone", joinColumns = {#JoinColumn(name="address_id",
referencedColumnName = "address_id") } )
private Addresses address;
Related
Although Lazy loading is applied, still it is running all 4 queries,
but I'm fetching only:
System.out.println("Author's Name: " + b.getEmail());
public class Author {
#Id
#Column(name = "AUTHOR_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private String email;
#OneToOne(mappedBy = "author", fetch = FetchType.LAZY)
private Book book;
#OneToOne(mappedBy = "author", fetch = FetchType.LAZY)
private Address address;
#OneToOne(mappedBy = "author", fetch = FetchType.LAZY)
private Location location;
}
public class BooksManager {
public static void main(String[] args) {
Author b = (Author) session.get(Author.class, 6L);
System.out.println("Author's Name: " + b.getEmail());
}
}
It's running all 4 queries for the above code:
Hibernate: select author0_.AUTHOR_ID as AUTHOR_I1_1_0_, author0_.email as email2_1_0_, author0_.name as name3_1_0_ from AUTHOR author0_ where author0_.AUTHOR_ID=?
Hibernate: select address0_.address_id as address_1_0_0_, address0_.description as descript2_0_0_, address0_.PUBLISHED as PUBLISHE3_0_0_, address0_.title as title4_0_0_ from ADDRESS address0_ where address0_.address_id=?
Hibernate: select book0_.book_id as book_id1_2_0_, book0_.description as descript2_2_0_, book0_.PUBLISHED as PUBLISHE3_2_0_, book0_.title as title4_2_0_ from BOOK book0_ where book0_.book_id=?
Hibernate: select location0_.location_id as location1_3_0_, location0_.description as descript2_3_0_, location0_.PUBLISHED as PUBLISHE3_3_0_, location0_.title as title4_3_0_ from LOCATION location0_ where location0_.location_id=?
In your definition it, all 3 OneToOne relationships are optional (nullable).
when hibernate loads the entity Author, it has to create 3 proxies for relationships. but if a property is null , hibernate should not create proxy for that property.
Now hibernate somehow has to know whether the property is null or not.
so it has to lookup all other 3 tables to detect that.
finally hibernate is intelligent enough to ignore the hint made for LAZY loading and load them eagerly.
App entity
#Entity
#Table(name = "app")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class App implements Serializable {
#ManyToMany
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#JoinTable(name = "app_alloweduser",
joinColumns = #JoinColumn(name="apps_id", referencedColumnName="id"),
inverseJoinColumns = #JoinColumn(name="allowedusers_id", referencedColumnName="id"))
private Set<User> allowedusers = new HashSet<>();
}
I try to write my own query as such.
#Query("SELECT app FROM App app WHERE (:department is null or app.department = :department) and "
+ "(:platform is null or app.platform = :platform) and "
+ "(:appName is null or lower(app.appName) LIKE CONCAT('%',lower(:appName),'%')) and "
+ "(app.id in (SELECT b.apps_id FROM allowedusers b where b.allowedusers_id = :loginid))")
Page<App> findAllAllowed(#Param("department") Department department,
#Param("platform") Platform platform,
#Param("appName") String appName,
#Param("loginid") Long loginid,
Pageable pageable);
Error: org.hibernate.hql.internal.ast.QuerySyntaxException: allowedusers is not mapped
I have tried app_alloweduser also and it too throws the same error that it can't map. The table APP_ALLOWEDUSER, which is what I am interested in is verified to be there though. So I am wondering what is going on here.
Some background
The table app_alloweduser has 2 column, app_id & user_id. I will first have to retrieve a list of app_id given a user_id. Then return all app from this list of app_id.
Try to replace FROM allowedusers by FROM app.allowedusers
There is no alloweduser entity only App and User. Try to use join instead.
#Query("SELECT app FROM App app join User u WHERE (:department is null or app.department = :department) and "
+ "(:platform is null or app.platform = :platform) and "
+ "(:appName is null or lower(app.appName) LIKE CONCAT('%',lower(:appName),'%')) and "
+ "(u.allowedusers_id = :loginid))")
UPDATE:
change u.allowedusers_id = :loginid to u.id = :loginid.
You don't have the table in HQL. Of course you can introduce a new entity if you need for the APP_ALLOWEDUSER
I found out that I can access the table via a.allowedusers
Given the following declaration or particularly this line,
inverseJoinColumns = #JoinColumn(name="allowedusers_id", referencedColumnName="id"))
I can access the database equivalent of app_alloweduser.allowedusers_id using a.allowedusers.id on JPQL.
#JoinTable(name = "app_alloweduser",
joinColumns = #JoinColumn(name="apps_id", referencedColumnName="id"),
inverseJoinColumns = #JoinColumn(name="allowedusers_id", referencedColumnName="id"))
private Set<User> allowedusers = new HashSet<>();
Final Query
#Query("SELECT a FROM App a JOIN a.allowedusers au WHERE au.id = :loginid AND "
+ "(:department is null or a.department = :department) AND "
+ "(:platform is null or a.platform = :platform) AND "
+ "(:appName is null or lower(a.appName) LIKE CONCAT('%',lower(:appName),'%'))")
Page<App> findAllAllowed(#Param("department") Department department,
#Param("platform") Platform platform,
#Param("appName") String appName,
#Param("loginid") Long loginid,
Pageable pageable);
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);
I am writing a JPQL query (with Hibernate as my JPA provider) to fetch an entity Company and several of its associations. This works fine with my "simple" ManyToMany associations, like so:
#Entity
#Table(name = "company")
#NamedQueries({
#NamedQuery(
name = "Company.profile.view.byId",
query = "SELECT c " +
"FROM Company AS c " +
"INNER JOIN FETCH c.city AS city " + <-- #ManyToOne
"LEFT JOIN FETCH c.acknowledgements " + <-- #ManyToMany
"LEFT JOIN FETCH c.industries " + <-- #ManyToMany
"WHERE c.id = :companyId"
)
})
public class Company { ... }
Hibernate creates a single query to fetch the above, which is good. However, my Company entity also has a many-to-many association with data stored in the intermediate table, hence why this is mapped as #OneToMany and #ManyToOne associations between three entities.
Company <-- CompanyService --> Service
These are the three entities that I have in my code. So a Company instance has a collection of CompanyService entities, which each has a relation to a Service instance. I hope that makes sense - otherwise please check the source code at the end of the question.
Now I would like to fetch the services for a given company by modifying the above query. I read in advance that JPA doesn't allow nested fetch joins or even aliases for joins, but that some JPA providers do support it, and so I tried my luck with Hibernate. I tried to modify the query as such:
#Entity
#Table(name = "company")
#NamedQueries({
#NamedQuery(
name = "Company.profile.view.byId",
query = "SELECT c " +
"FROM Company AS c " +
"INNER JOIN FETCH c.city AS city " +
"LEFT JOIN FETCH c.acknowledgements " +
"LEFT JOIN FETCH c.industries " +
"LEFT JOIN FETCH c.companyServices AS companyService " +
"LEFT JOIN FETCH companyService.service AS service " +
"WHERE c.id = :companyId"
)
})
public class Company { ... }
Now, instead of creating a single query, Hibernate creates the following queries:
#1
select ...
from company company0_
inner join City city1_ on company0_.postal_code = city1_.postal_code
[...]
left outer join company_service companyser6_ on company0_.id = companyser6_.company_id
left outer join service service7_ on companyser6_.service_id = service7_.id
where company0_.id = ?
#2
select ...
from company company0_
inner join City city1_ on company0_.postal_code = city1_.postal_code
where company0_.id = ?
#3
select service0_.id as id1_14_0_, service0_.default_description as default_2_14_0_, service0_.name as name3_14_0_
from service service0_
where service0_.id = ?
#4
select service0_.id as id1_14_0_, service0_.default_description as default_2_14_0_, service0_.name as name3_14_0_
from service service0_
where service0_.id = ?
Query #1
I left out the irrelevant joins as these are OK. It appears to select all of the data that I need, including the services and the intermediate entity data (CompanyService).
Query #2
This query simply fetches the company from the database and its City. The city association is eagerly fetched, but the query is still generated even if I change it to lazy fetching. So honestly I don't know what this query is for.
Query #3 + Query #4
These queries are looking up Service instances based on ID, presumably based on service IDs fetched in Query #1. I don't see the need for this query, because this data was already fetched in Query #1 (just as the data from Query #2 was already fetched in Query #1). Also, this approach obviously does not scale well if a company has many services.
The strange thing is that it seems like query #1 does what I want, or at least it fetches the data that I need. I just don't know why Hibernate creates query #2, #3 and #4. So I have the following questions:
Why does Hibernate create query #2, #3 and #4? And can I avoid it?
Does Hibernate support nested association fetching even though JPA doesn't? If so, how would I go about it in my case?
Is this behavior normal, or is it because what I am trying to do is just not supported, and therefore I get weird results? This would seem odd, because query #1 looks perfectly fine
Any pointers of mistakes or alternative solutions to accomplish what I want would be much appreciated. Below is my code (getters and setters excluded). Thanks a lot in advance!
Company entity
#Entity
#Table(name = "company")
#NamedQueries({
#NamedQuery(
name = "Company.profile.view.byId",
query = "SELECT c " +
"FROM Company AS c " +
"INNER JOIN FETCH c.city AS city " +
"LEFT JOIN FETCH c.acknowledgements " +
"LEFT JOIN FETCH c.industries " +
"LEFT JOIN FETCH c.companyServices AS companyService " +
"LEFT JOIN FETCH companyService.service AS service " +
"WHERE c.id = :companyId"
)
})
public class Company {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column
private int id;
// ...
#ManyToOne(fetch = FetchType.EAGER, targetEntity = City.class, optional = false)
#JoinColumn(name = "postal_code")
private City city;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "company_acknowledgement", joinColumns = #JoinColumn(name = "company_id"), inverseJoinColumns = #JoinColumn(name = "acknowledgement_id"))
private Set<Acknowledgement> acknowledgements;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "company_industry", joinColumns = #JoinColumn(name = "company_id"), inverseJoinColumns = #JoinColumn(name = "industry_id"))
private Set<Industry> industries;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "company")
private Set<CompanyService> companyServices;
}
CompanyService entity
#Entity
#Table(name = "company_service")
#IdClass(CompanyServicePK.class)
public class CompanyService implements Serializable {
#Id
#ManyToOne(targetEntity = Company.class)
#JoinColumn(name = "company_id")
private Company company;
#Id
#ManyToOne(targetEntity = Service.class)
#JoinColumn(name = "service_id")
private Service service;
#Column
private String description;
}
Service entity
#Entity
#Table(name = "service")
public class Service {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column
private int id;
#Column(length = 50, nullable = false)
private String name;
#Column(name = "default_description", nullable = false)
private String defaultDescription;
}
Fetching data
public Company fetchTestCompany() {
TypedQuery<Company> query = this.getEntityManager().createNamedQuery("Company.profile.view.byId", Company.class);
query.setParameter("companyId", 123);
return query.getSingleResult();
}
Okay, it seems like I figured it out. By setting the fetch type to FetchType.LAZY in CompanyService, Hibernate stopped generating all of the redundant queries that were basically fetching the same data again. Here is the new version of the entity:
#Entity
#Table(name = "company_service")
#IdClass(CompanyServicePK.class)
public class CompanyService implements Serializable {
#Id
#ManyToOne(fetch = FetchType.LAZY, targetEntity = Company.class)
#JoinColumn(name = "company_id")
private Company company;
#Id
#ManyToOne(fetch = FetchType.LAZY, targetEntity = Service.class)
#JoinColumn(name = "service_id")
private Service service;
#Column
private String description;
}
The JPQL query remains the same.
However, in my particular case with the number of associations my Company entity has, I was getting a lot of duplicated data back, and so it was more efficient to let Hibernate execute an additional query. I accomplished this by removing the two join fetches from my JPQL query and changing my query code to the below.
#Transactional
public Company fetchTestCompany() {
TypedQuery<Company> query = this.getEntityManager().createNamedQuery("Company.profile.view.byId", Company.class);
query.setParameter("companyId", 123);
try {
Company company = query.getSingleResult();
Hibernate.initialize(company.getCompanyServices());
return company;
} catch (NoResultException nre) {
return null;
}
}
By initializing the companyServices association, Hibernate executes another query to fetch the services. In my particular use case, this is better than fetching a ton of redundant data with one query.
I hope this helps someone. If anyone has any better solutions/improvements, then I would of course be happy to hear them.
From what you wrote, I would say that nested fetching isn't supported. This is my understanding of your results:
Query #1 is ok, and joins everything that it needs, this is good
However, Query #2 I think gets CompanyService#company (with eager city resulting in inner join City)
Query #3 gets CompanyService#service
Query #4 is a mistery to me
I know this is not an answer, but it might help you understand what's going on in the background.
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.