Hibernate many-to-many join with condition - java

+-------+ +--------------| +-------+
| BOY | | BOY_GIRL | | GIRL |
+-------+ +--------------| +-------+
| id | | id | | id |
| name | | boy_id | | name |
| birth | | girl_id | | birth |
+-------+ | start_dating | +-------+
+--------------|
START_DATING is type of TIMESTAMP or DATE
I have two beans Boy and Girl with many-to-many relation
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "BOY_GIRL", joinColumns = {#JoinColumn(name = "BOY_ID")}, inverseJoinColumns = {#JoinColumn(name = "GIRL_ID")})
public Set<Girl> getGirls() {
return girls;
}
Now, how can I do select query with HQL, if I want to get the list girls with condition:
where boy_id = (some_boy_id) and START_DATING > (some_timestamp)

I think you have to create a BoyGirl class because table BOY_GIRL is not a simple many-to-many table (If it is, then the columns are has to be only boy_id and girl_id). So what you should do is create the BoyGirl class then map BOY to BOY_GIRL with one-to-many and also map GIRL to BOY_GIRL with one-to-many
table relations
+-------+ +--------------+ +-------+
| BOY | | BOY_GIRL | | GIRL |
+-------+ +--------------| +-------+
| id | 0..* --- 1..1 | id | 1..1 --- 0..* | id |
| name | | boy_id | | name |
| birth | | girl_id | | birth |
+-------+ | start_dating | +-------+
+--------------+
java classes
public class BoyGirl {
private long id;
private Boy boy;
private Girl girl;
private Date startDating;
}
public class Boy {
//other attributes omitted
private Set<BoyGirl> boyGirls;
}
public class Girl {
//other attributes omitted
private Set<BoyGirl> boyGirls;
}
The select query you need
// I'm using criteria here, but it will have the same result as your HQL
public List getGirls(Boy boy, Date startDating) {
Criteria c = sessionFactory.getCurrentSession().createCriteria(BoyGirl.class);
c.add(Restrictions.eq("boy.id", boy.getId());
c.add(Restrictions.lt("startDating", startDating);
List<BoyGirl> boyGirls = (List<BoyGirl>) c.list();
// at this point you have lazily fetch girl attributes
// if you need the girl attributes to be initialized uncomment line below
// for (BoyGirl boyGirl : boyGirls) Hibernate.initialize(boyGirl.getGirl());
return boyGirls;
}

I think your entity model is not correct, you need a third entity representing the relationship attribute and you should map both Boy and Girl as many to one to that entity.
Otherwise there is no way to specify the relationship attribute, in your case starting_date, as a condition in a query.
Look at this link, you can find a detailed explanation on how to map a join table with additional attributes.

As the intermediate table BOY_GIRL has some additional attributes (start_dating) you need to create another intermediate entity in your domain model for it, eg:
#Table(name="BOY_GIRL")
class Relationship {
#Id
long id;
#ManyToOne
Boy boy;
#ManyToOne;
Girl girl;
#Column
Date startDating;
}

I don't think its great to keep the dating information directly inside the join table (since the purpose of this should simply be to associate a boy and a girl).
However, if you really want to keep your current structure, and solve it with HQL, you should be able to do something like
SELECT g FROM GIRL g, BOY_GIRL bg
WHERE bg.start_dating = :revelantdate
AND bg.boy_id = :boyid
AND g.id = bg.girl_id

Related

Hibernate, how to do a many-to-one mapping on a may-be-unique column, ignoring duplication

I have 2 Hibernate entities:
#Entity
class Car {
#ManyToOne
#JoinColumn(name = "brand_code")
private BrandReference brand;
}
#Entity
class BrandReference {
#Column(name = "brand_code")
private String brandCode;
}
The table BrandReference will have data like this:
row_id | brand_code | brand_name
--------------------------------
1 | TOYO | Toyota
2 | BENZ | Mercedes-Benz
3 | HOND | Honda
The column brand_code actually is expected to be unique. But I am in a situation where this expectation cannot be controlled. The data in table BrandReference is controlled by external party and it is unavoidable to have an error that cause the table to have more than 1 rows with the same brand_code
row_id | brand_code | brand_name
--------------------------------
1 | TOYO | Toyota
2 | BENZ | Mercedes-Benz
3 | HOND | Honda
4 | HOND | Gonda (Typo)
I don't like the program to crash on this situation. Is there a way to tell Hibernate to choose the row with the least row_id in case it found more than 1 rows with the matched brand_code when assigning a BrandReference to a Car. Something like this.
#Entity
class Car {
#ManyToOne
#JoinColumn(name = "brand_code")
#GetFirstRow(orderBy = "row_id")
private BrandReference brand;
}

Delete a row only into the join table

Question
Is it possible to delete a row into a join table created by #ManyToMany annotation?
Context
With this schema :
TAG
TAGS_ARTICLES
ARTICLE
When a tag is removed from public Set<Tag> tags (a list into ARTICLE class) corresponding rows into TAGS_ARTICLES is removed too, but not the tag into TAG table.
The only way is to create a SQL script or JPA/Hibernate allow us to do that with annotations?
Code
My current code is : article.getTags().remove(tag);
This line remove the tag from the list, but the change is not done in database.
Conclusion
I saw this post : How to delete a row in join table with JPA , but relative tag must be deleted too (not my case).
Thanks.
Edit 1 : Expected result in database
Before delete
ARTICLE
| article_id |
| a1 |
| a2 |
| a3 |
TAGS_ARTICLES
| article_id | tag_id |
| a1 | t1 |
| a1 | t2 |
| a2 | t2 |
TAG
| article_id |
| t1 |
| t2 |
After delete t1 from a1 tag list
ARTICLE
| article_id |
| a1 |
| a2 |
| a3 |
TAGS_ARTICLES
| article_id | tag_id |
| a2 | t1 |
| a2 | t2 |
TAG
| article_id |
| t1 |
| t2 |
Edit 2 : Join table code
#Entity
public class Article {
...
#ManyToMany
#JoinTable(name = "tags_articles",
joinColumns = #JoinColumn(name = "idarticle"),
inverseJoinColumns = #JoinColumn(name = "idtag")
)
private Set<Tag> tags = new HashSet<>();
...
}
Edit: see comments
Using this set-up should produce the wanted result
class Article {
...
#ManyToMany
#JoinTable(...)
private Set<Tag> tags = new HashSet<>();
}
class Tag {
...
#ManyToMany(mappedBy = "tags")
private Set<Article> articles = new HashSet<>();
}
The Article entity is taking ownership of the relationship.
Old answer.
When a tag is removed from public Set<Tag> tags (a list into ARTICLE
class) the corresponding row into TAGS_ARTICLES is removed too, but
not the tag into TAG table.
By this I understand that the orphaned records are not deleted. And you want to delete them. Is this correct?
You might want to try using the Hibernate specific #Cascade annotation (documentation).
Just annotate your Collection<T> field.
#ManyToMany(...)
#Cascade(CascadeType.REMOVE) // or CascadeType.DELETE
private Set<Tag> tags = new HashSet<>();
Be sure to include it from the org.hibernate.annotations package.
The behavior of entity operation is depends on ownership of the relation, which is determined by where you place the mappedBy attribute to the annotation. Entity having mappedBy is the one which is not the owner. Both side of relationship cannot be owner.
Here you need to decide the correct owner. Let say the Tag is the owner. Then when deleting a Tag the relation TAGS_ARTICLES will be updated automatically. when deleting a TAGS_ARTICLES you have to take care of deleting the relation yourself.
#Entity
public class Tag{
#ManyToMany
Set<Tag_Articles> articles;
//...
}
#Entity
public class Tag_Articles{
#ManyToMany(mappedBy="articles")
Set<Tag> tags;
//...
}
For the entity relationship like above, you can try something like this -
entityManager.remove(articles)
for (Tag tag: articles.tags) {
tag.articiles.remove(articles);
}

How to do a hibernate saving without duplicate records in a different table

Can someone help on the following scenario, please? Let say I have table 1
|----------------|--------------|------------|--------|
| Salesman | Product | Year | Sold |
|----------------|--------------|------------|--------|
| John | prod1 | 2015 | 2000 |
|----------------|--------------|------------|--------|
| John | prod1 | 2016 | 2000 |
|----------------|--------------|------------|--------|
| John | prod2 | 2015 | 2000 |
|----------------|--------------|------------|--------|
| John | prod2 | 2016 | 2000 |
|----------------|--------------|------------|--------|
| Tracy | prod1 | 2015 | 2000 |
|----------------|--------------|------------|--------|
| Tracy | prod1 | 2016 | 2000 |
|----------------|--------------|------------|--------|
| Tracy | prod2 | 2015 | 2000 |
|----------------|--------------|------------|--------|
| Tracy | prod2 | 2016 | 2000 |
|----------------|--------------|------------|--------|
As you can see in table 1, Salesman, Product are not unique key. However, now I want to do a saving to table 2 with some detailed info for Salesman and Product combination. In other words, I want the Salesman and product is unique in table 2.
|----------------|--------------|--------|
| Salesman | Product |priority|
|----------------|--------------|--------|
| John | prod1 | true |
|----------------|--------------|--------|
| John | prod2 | false |
|----------------|--------------|--------|
| Tracy | prod1 | true |
|----------------|--------------|--------|
| Tracy | prod2 | true |
|----------------|--------------|--------|
Is there anyway I can use the following entity for hibernate.
#Entity
#Table(name = "table1")
public class EntityTable1() {
private String salesman;
private String product;
private int year;
private int sold;
private List<EntityTable2> entityTable2;
....
}
and
#Entity
#Table(name = "table2")
public class EntityTable2() {
private String salesman;
private String product;
private boolean priorityTarget;
....
}
Is there anyway, when I save EntityTable1 objects, I save EntityTable2 cascade and also make sure table2 doesn't have duplicate rows? I understand there will be other ways to change the entity or DB design. However, since I am working on a project that have been built and used by other team, I cannot simply change table 1 schema. But I can do whatever for table 2. Please help. Thanks.
It very much depends on what you want to be unique in EntityTable2. Do you want the combination of all your fields to be unique? In that case you would need something like this:
#Table(
name = "table2",
uniqueConstraints = {#UniqueConstraint(columnNames = {"salesman", "product", "priorityTarget"})}
)
Do you want the individual fields to be unique? Then you would want something like this:
#Table(
name = "table2",
uniqueConstraints = {
#UniqueConstraint(columnNames = "salesman"),
#UniqueConstraint(columnNames = "product"),
#UniqueConstraint(columnNames = "priorityTarget")
}
)
And of course you can mix and match the above.
Hope this helps!
You should be able to achieve this using Hibernate Interceptor. You can save the objects of table2 in the onSave event of table1.
For uniqueness you will need to check if the record exists, if it exists, update the same record else create a new one.

Query Predicate in QueryDSL

The environment is Java, Spring-boot, Hibernat, QueryDSL, MySQL.
I have table structure
Episode
+----+-------------+--------
| id | address_id | eventno
+----+-------------+--------
| 5 | 27 | F123
| 6 | 30 | F456
| 7 | 45 | F789
+----+-------------+--------
#Entity
public class Episode {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotEmpty
private String eventno;
#ManyToOne(cascade = CascadeType.ALL)
private Address address;
Episode_Person
+----+--------------+--------------+------------+-----------+
| id | episode_role | primary_flag | episode_id | person_id |
+----+--------------+--------------+------------+-----------+
| 19 | Buyer | | 5 | 1 |
| 20 | Subject | | 5 | 2 |
| 23 | Witness | | 6 | 3 |
| 24 | Child | | 6 | 4 |
| 27 | Buyer | | 5 | 3 |
| 63 | Investor | | 5 | 4 |
| 64 | Subject | | 7 | 1 |
| 65 | Subject | | 7 | 3 |
#Entity
public class EpisodePerson {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#Valid
private Person person;
#ManyToOne
private Episode episode;
Person
+----+-----------+----------+
| id | firstname | surname |
+----+-----------+----------+
| 1 | Clint | eastwood |
| 2 | Angelina | joilee |
| 3 | Brad | pitt |
| 4 | Jennifer | aniston |
#Entity
#Table(uniqueConstraints = #UniqueConstraint(columnNames = {"nia"}))
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String surname;
private String firstname;
private String gender;
So each episode has multiple people. And the join table is Episode_Person.
My UI has a datatable with a filter on each column:
The filtering already works on Event and Address. And looks like this predicate in QueryDSL:
BooleanBuilder where = new BooleanBuilder();
if (pagination.getFilterBy().getMapOfFilters().get("eventno")!=null) {
where.and(qEpisode.eventno.containsIgnoreCase(pagination.getFilterBy().getMapOfFilters().get("eventno")));
}
if (pagination.getFilterBy().getMapOfFilters().get("address")!=null) {
where.and(qEpisode.address.formattedAddress.containsIgnoreCase(pagination.getFilterBy().getMapOfFilters().get("address")));
}
where.and(qEpisode.creatingUser.eq(user));
List<Episode> e = episodeRepository.findAll(where);
How would I now add a 3rd predicate for case name where case name is constructed of the first two people returned in the collection of people against a episode?
UPDATE
For clarification the DTO thats backs the UI view contains the "casename" attribute. It is created in the service layer when Domain objects are converted to DTO:
episodeDashboard.setNames(episodePersonList.get(0).getPerson().getSurname().toUpperCase() +" & " +episodePersonList.get(1).getPerson().getSurname().toUpperCase());
Not easily unless you delegate some of the processing to the database.
If we can get the case_name property to be populated at the database tier rather than as a derived property in the application logic then the front-end code becomes trivial.
We can do this by means of a view. The exact definition of this will depend on your database however the output would be something like this:
episode_summary_vw
+------------+-------------------------+
| epsiode_id | case_name |
+------------+-------------------------+
| 5 | Eastwood & Joilee|
| 6 | Pitt & Aniston|
| 7 | Aniston & Pitt|
+------------+-------------------------+
For Oracle it looks like LISTAGG function is what you would want and for MySQL the GROUP_CONCAT functions. In MySQL then I think this would look something like:
CREATE VIEW episode_summary_vw as
SELECT ep.episode_id, GROUP_CONCAT(p.surname SEPARATOR ' & ')
FROM episode_person ep
INNER JOIN person p on p.id = ep.person_id
GROUP BY ep.episode_id;
-- todo: needs limit to first 2 records
Once we have a view then we can simply map the case_name to the Episode entity using the #SecondaryTable functionality of JPA:
#Entity
#Table(name = "episodes")
#SecondaryTable(name = "episode_summary_vw", primaryKeyJoinColumna = #PrimaryKeyJoinColumn(name="episode_id", reference_column_name="id"))
public class Episode {
#Column(name ="case_name", table = "episode_summary_vw")
private String caseName;
}
You then filter and sort on the property as for any other field:
if (pagination.getFilterBy().getMapOfFilters().get("caseName")!=null) {
where.and(qEpisode.caseName.containsIgnoreCase(pagination.getFilterBy().
getMapOfFilters().get("caseName")));
}

Dynamic #Table name based on input

We have Hibernate based application where due to a large data set, two sets of tables are created where user_id will either be mapped in the UserTickets table or RestOfWorldTickets table.
Would like to know how #Table on the entity java objects can be dynamically mapped based on some user selection.
#Entity
#Table(name = "**UserTickets**")
public class UserTickets {
#Id
#Column("Internal_id")
#GeneratedValue(strategy = IDENTITY)
private int internalId;
#Column("user_id")
private int userId;
#Column("state")
private String state;
#Column("city")
private String city;
#Column("address")
private String address;
#Column("ticketNumber")
private String ticketNumber;
..
// Setters and Getters
}
UserTickets DB Table
Internal_id | User_id | State | City | Address | ticket_number | ...
101 | 1025 | AZ | Tuscan | .. | 10256912 |
102 | 1026 | NC | Durham | .. | 10256983
RestOfWorldTickets DB Table
Internal_id | User_id | State | City | Address | ticket_number |..
101 | 1058 | {null} | London | .. | 102578963 |..
102 | 1059 | {null} | Berlin | .. | 112763458 |..
The user and table mapping are now defined in a new table.
TableMapping Database table.
Internal_id | User_id | TableMapped |
1 | 1025 | UserTickets |
2 | 1026 | UserTickets |
3 | 1058 | RestOfWorldTickets |
4 | 1059 | RestOfWorldTickets |
So, using the UserTickets result set, how I map #Table attribute on the UserTickets Java object dynamically so that my Criteria API queries will work automatically without changing them to HQL queries?
Maybe using Interceptors http://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/Interceptor.html?
I am quite unsure what you actually need but i try to give my solution based on a few quesses. Changing #Table dynamically is not -afaik- possible but if i guessed right you could have some benefit of inheritance in this case:
1st modify UserTickets to allow inheritance
#Entity
//#Table(name = "**UserTickets**")
#Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class UserTickets {
#Id // this annotation was missing from yours ?
#Column(name="Internal_id")
#GeneratedValue(strategy = GenerationType.SEQUENCE)
// identity generated problems in openjpa so i changed it to SEQUENCE
private int internalId;
#Column(name="user_id") private int userId;
#Column(name="state") private String state;
#Column(name="city") private String city;
#Column(name="address") private String address;
#Column(name="ticketNumber") private String ticketNumber;
}
2nd create a new entity
#Entity
public class RestOfWorldTickets extends UserTickets {
// yes i am just an empty class, TABLE_PER_CLASS gives me the fields
}
This allows you to use criteriaqueries against UserTickets but in addition the queries are done against RestOfWorldTickets also. So now when you search with user id result set will contain results from both tables. Checking/ loggin -for example with instanceof operator- you can see which one the ticket is.
"Disclaimer" i am using and testing with openjpa so there can be some differences/probkems with this solution...

Categories