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);
}
Related
I am using QueryDSL within a Spring Boot, Spring Data JPA project.
I have the following schema for a table called test:
| id | key | value |
|----|------|-------|
| 1 | test | hello |
| 1 | test | world |
| 2 | test | hello |
| 2 | foo | bar |
| 3 | test | hello |
| 3 | test | world |
Now I want to write the following SQL in QueryDSL:
select id from test where key = 'test' and value = 'hello'
INTERSECT
select id from test where key = 'test' and value = 'world'
Which would give me all ids where key is 'test' and values are 'hello' and 'world'.
I did not find any way of declaring this kind of SQL in QueryDSL yet. I am able to write the two select statements but then I am stuck at combining them with an INTERSECT.
JPAQueryFactory queryFactory = new JPAQueryFactory(em); // em is an EntityManager
QTestEntity qTestEntity = QTestEntity.testEntity;
var q1 = queryFactory.query().from(qTestEntity).select(qTestEntity.id).where(qTestEntity.key("test").and(qTestEntity.value.eq("hello")));
var q2 = queryFactory.query().from(qTestEntity).select(qTestEntity.id).where(qTestEntity.key("test").and(qTestEntity.value.eq("world")));;
In the end I want to retrieve a list of ids which match the given query. In general the amount of intersects can be something around 20 or 30, depending on the number of key/value-pairs I want to search for.
Does anyone know a way how to do something like this with QueryDSL ?
EDIT:
Assume the following schema now, with two tables: test and 'user':
test:
| userId | key | value |
|---------|------|-------|
| 1 | test | hello |
| 1 | test | world |
| 2 | test | hello |
| 2 | foo | bar |
| 3 | test | hello |
| 3 | test | world |
user:
| id | name |
|----|----------|
| 1 | John |
| 2 | Anna |
| 3 | Felicita |
The correspond java classes look like this. TestEntity has a composite key consisting of all of its properties.
#Entity
public class TestEntity {
#Id
#Column(name = "userId", nullable = false)
private String pubmedId;
#Id
#Column(name = "value", nullable = false)
private String value;
#Id
#Column(name = "key", nullable = false)
private String key;
}
#Entity
class User {
#Id
private int id;
private String name;
#ElementCollection
private Set<TestEntity> keyValues;
}
How can I map the test table to the keyValues properties within the User class?
Your TestEntity is not really an Entity, since it's id is not a primary key, it's the foreign key to the user table.
If it's only identifiable by using all its properties, it's an #Embeddable, and doesn't have any #Id properties.
You can map a collection of Embeddables as an #ElementCollection part of another entity which has the id as primary key. The id column in your case is not a property of the Embeddable, it's just the foreign key to the main table, so you map it as a #JoinColumn:
#Embeddable
public class TestEmbeddable {
#Column(name = "value", nullable = false)
private String value;
#Column(name = "key", nullable = false)
private String key;
}
#Entity
class User {
#Id
private int id;
#ElementCollection
#CollectionTable(
name="test",
joinColumns=#JoinColumn(name="id")
)
private Set<TestEmbeddable> keyValues;
}
In this case, the QueryDSL becomes something like this (don't know the exact api):
user.keyValues.any().in(new TestEmbeddable("test", "hello"))
.and(user.keyValues.keyValues.any().in(new TestEmbeddable("test", "world"))
In this case I'd probably just use an OR expression:
queryFactory
.query()
.from(qTestEntity) .select(qTestEntity.id)
.where(qTestEntity.key("test").and(
qTestEntity.value.eq("hello")
.or(qTestEntity.value.eq("world")));
However, you specifically mention wanting to use a set operation. I by the way think you want to perform an UNION operation instead of an INSERSECT operation, because the latter one would be empty with the example given.
JPA doesn't support set operations such as defined in ANSI SQL. However, Blaze-Persistence is an extension that integrates with most JPA implementations and does extend JPQL with set operations. I have recently written a QueryDSL extension for Blaze-Persistence. Using that extension, you can do:
List<Document> documents = new BlazeJPAQuery<Document>(entityManager, cbf)
.union(
select(document).from(document).where(document.id.eq(41L)),
select(document).from(document).where(document.id.eq(42L))
).fetch();
For more information about the integration and how to set it up, the documentation is available at https://persistence.blazebit.com/documentation/1.5/core/manual/en_US/index.html#querydsl-integration
After many attempts I'm here to seek a help from any of you. A solution is very much needed.
I have a parent (Patient) entity and its child (Address) entity.
Where I want to fetch all the registered patients and address details, where address details may be null. Meaning, Patient may not have address details..
I have written logic by using CriteriaBuilder like this
CriteriaBuilder builder = this.em.getCriteriaBuilder();
CriteriaQuery<Object[]> query = builder.createQuery(Object.class);
Root<TaskImpl> patientRoot = query.from(Patient.class);
Root<ContentImpl> addressRoot = query.from(Address.class);
query.multiselect(patientRoot.get("patinetId"),patientRoot .get("patinetName"),
addressRoot.get("city));
Predicate patAddressJoinPred = builder.equal(
patientRoot.get("patientId"),
addressRoot.get("patient").get("patientId"));
query.where(builder.and(patAddressJoinPred));
please find Patient and Address entities for your ref,
Patient.java-----------------
#Entity
public class Patinet{
#Id
private Long patentId;
private String patientName;
}
------------------------------
Address.java------------------
#Entity
public class Address{
#Id
private Long addressId;
private String city;
#OneToOne(FetchType.Lazy)
#JoinColumn("patinet_id")
private Patient patient;
}
-------------------------------
But after the criteria builder, I have applied cross join on Address entity which will be a performance problem and I cannot get details of patients which don't have address details..
for simplicity, the sample data and my required output is given bellow.
Patient table
-------------
id | name
-------------
1 | Sameul
2 | Jhon
3 | khan
4 | Lee
-------------
Address table
-----------------------
id | city | patient_id
-----------------------
1 | Blz | 1
2 | Stn | 3
required out put
-------------------------
id | patientName | city
-------------------------
1 | Sameul | blz
2 | Jhon |
3 | khan | stn
4 | Lee |
But getting like this
-------------------------
id | patientName | city
-------------------------
1 | Sameul | blz
3 | khan | stn
-------------------------
Will be waiting for your valuable solution
Thank you..
It looks you are using inner join, you should use outer joins
Something like,
final Root<Patient> patient = criteriaQuery.from(Patient.class);
Join<Patient, Address> join1 = patient .join("joinColumnName", JoinType.LEFT);
Predicate predicate = criteriaBuilder.equal(Address.<String> get("patient_id"), patient;
criteria.add(predicate);
criteriaQuery.where(predicate);
criteriaQuery.distinct(true);
Hope it resolves your query. I have not tested it but should work fine.
first of all, I'd like to point out I'm new to Spring and Hibernate.
I've been trying to connect Hibernate with MySQL (using Spring Data), which I've done successfuly, however, when I try to do an insert I can't get it to do what I want.
What I'm trying to accomplish is to have a User class, with an ArrayList that contains their preferred coding languages.
For example:
User 1: C, Java, C++
User 2: Python, Java, C++
However, I've noticed that Hibernate will create a new row in the "Languages" table even if it's already there. I also want to keep the "id" as a primary key and as a numeric value.
The following is how it currently inserts the languages to the table:
+----+--------+
| id | name |
+----+--------+
| 2 | C |
| 3 | Java |
| 4 | C++ |
| 6 | Python |
| 7 | Java |
| 8 | C++ |
+----+--------+
This is how I want them:
+----+--------+
| id | name |
+----+--------+
| 2 | C |
| 3 | Java |
| 4 | C++ |
| 6 | Python |
+----+--------+
I've tried different methods, but it's always repeating the values...
This is my user class:
#Entity
public class User {
#Id
#GeneratedValue
private int id;
private String username;
#OneToMany(cascade = CascadeType.ALL)
private List<Language> languages;
...
}
And this is my Language class:
#Entity
public class Language {
#Id
#GeneratedValue
private int id;
private String name;
...
}
This is how I'm adding the languages to each user (it's just a test, not final code):
ArrayList<Language> languages = new ArrayList<>();
languages.add(new Language("C"));
languages.add(new Language("Java"));
languages.add(new Language("C++"));
Image image = null;
User admin = new User("iscle", "albertiscle9#gmail.com", "Test_Password", languages, image);
userRepository.save(admin);
ArrayList<Language> languages2 = new ArrayList<>();
languages2.add(new Language("Python"));
languages2.add(new Language("Java"));
languages2.add(new Language("C++"));
Image image2 = null;
User admin2 = new User("iscle", "albertiscle9#gmail.com", "Test_Password", languages2, image2);
userRepository.save(admin2);
make attribute name (Languages) "UNIQUE"; then you ensure that there will be no duplicate
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...
+-------+ +--------------| +-------+
| 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