I have tables Tags, Blog and BlogTags.
With using many-to-many I would like a blog to have many tags (as objects with id, name).
In front end, when I add tags, they are created with name only and id of null so when I save the blog, new tag gets created every time as it auto increments Tag, what I would like is for the Tag to be merged with blog if said tag already exists. The idea is so that I can click on any give tag and get the all the blogs associated with it.
So object sent to backend would be:
{id: null,
name: 'name'
,.....
tags: [{id: null, name: 'name},{id: null, name: 'name}]}
Any help is appreciated.
#Data
#Entity
public class Tag {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#ManyToMany(mappedBy = "tags", fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
private List<BlogPost> blogPosts;
}
public class BlogPost {
//other data
#ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
#JoinTable(
name = "blog_post_tag",
joinColumns = {#JoinColumn(name = "blog_post_id")},
inverseJoinColumns = {#JoinColumn(name = "tag_id")})
private List<Tag> tags;
}
I went with #ElementCollection without many-to-many annotations and it works.
Not having the TAG ID is the normal behavior to register.
You can try, which for me is the best option, to load a master table from which you get the id if it already exists and with a find (depending on the technology used) at the moment the user inserts the check if tag. exists to load the id, get the DB entity and set the relationship of the tag entity to the post
Or before saving the entity, post iterate over all the tags making a query by tag to see if it exists and assign it.
In essence it is the same but with the first option we only make a query to DB
Related
I'm trying to get all Posts which don't contain certain category using QueryDsl
My models are defined as:
Post
#QueryEntity
#Table(name = "posts")
public class PostEntity implements {
#Id
#Column(name = "id")
private String id;
#OneToMany
#JoinTable(
name = "post_categories",
joinColumns = #JoinColumn(name = "post_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "category_id", referencedColumnName = "id")
)
private List<CategoryEntity> categories;
}
Category
#QueryEntity
#Table(name = "categories")
public class CategoryEntity {
#Id
#Column
private String id;
}
(some Lombok annotations omitted for brevity)
The two are related through post_categories join table to tag posts with categories.
I've tried using the query similar to this one, to exclude posts categorised as news:
var query = QPostEntity
.postEntity
.categories.any().id.notIn("news");
However that still returns posts in that category - only way I got it to work properly is to include all post categories in notIn statement.
Question: How do I query for Posts which don't contain specific category?
Update #1
Seems the query above generates subquery similar to
where exists(
select 1 from post_categories where category_id not in ('news')
)
which also includes all the posts with other categories. I found the following query does produce correct results (not moved before exists statement):
where not exists(
select 1 from post_categories where category_id in ('news')
)
Which can be done by rewriting querydsl as:
.categories.any().id.in("news").not();
However that seems to be very confusing. Any better way of doing it?
I would try to solve this with subqueries. Can you try the following?
SubQueryExpression<String> subquery = JPAExpressions.select(QCategoryEntity.categoryEntity.id)
.from(QCategoryEntity.categoryEntity)
.where(CategoryEntity.categoryEntity.eq("news"));
return new JPAQueryFactory(em)
.select(QPostEntity.postEntity)
.from(QPostEntity.postEntity)
.innerJoin(QPostEntity.postEntity.categories)
.where(QCategoryEntity.categoryEntity.id.notIn(subquery));
Probably you are not using the JPAQueryFactory... if not, could you share how you are actually performing the query?
Hello Hibernate/JPA gurus!
I am new to Hibernate, and have a question.
Say I have a bidirection relationship between Books and Tags (any book can be tagged with any tag)
Tables:
book (bookid, bookname)
tag (tagid, tagname)
booktaglink (booktaglinkid, bookid, tagid)
public class Book() {
#ManyToMany(fetch = FetchType.LAZY)
#Cascade({CascadeType.MERGE, CascadeType.PERSIST, CascadeType.EVICT})
#JoinTable(name = "booktaglink",
joinColumns = {#JoinColumn(name = "bookid") },
inverseJoinColumns = {#JoinColumn(name = "tagid") })
private List<Tag> tags = new ArrayList()
}
public class Tag() {
#ManyToMany(fetch = FetchType.LAZY, mappedBy="tags")
#Cascade({CascadeType.MERGE, CascadeType.PERSIST, CascadeType.EVICT})
private List<Book> books = new ArrayList()
}
So, above... I have Books and Tags bidirectional relationship, so I can retrieve tags that are associated with the book, and books that are associated with the tags. However, Books owns the relationship.
Say I want to delete a tag. Is this correct?
Tag tagToDelete;
List<Book> books = tag.getBooks()
for (Book book: books) {
book.getTags().remove(tagToDelete);
dataSource.save(book)
}
dataSource.delete(tagToDelete);
Why do I have to open the owner of association (in this case Book class) and remove the tag I am trying to delete? Can I simply cascade the delete and remove all associations with books? This sucks because if I simply do dataSource.delete(tagToDelete), it will leave all the associations in the link table, and cause errors. Is there any way to automate the delete process instead of looping as I did in the example above.
Is there a general rule about who should own the relationship?
Why would anyone create a uni-directional relationship? If this was unidirectional, and I am trying to delete a tag, I will never be able to delete the associations except if I loop through all the books in the database and remove the tag I am deleting. Seems inefficient.
Thanks so much!!
PA
Deleting an entity in a bidirectional ManyToMany relation is not that intuitive. From what i recall, when you delete an entity from the owning side (Book in your case), all joins are deleted as well without deleting any Tags. Vize versa this won't work. To solve this, just declare both sides as owning sides. Plus I advise you to use Sets to prevent duplicate data.
In your class Tag get rid of mappedBy="tags" and add a #JoinTable:
#JoinTable(name = "booktaglink",
joinColumns = {#JoinColumn(name = "tagid") },
inverseJoinColumns = {#JoinColumn(name = "bookid") })
private Set<Book> books;
I am having following problem. I have a user entity that has a many to many relationship with other user entities. Hence I want to make a self-join with manytomany annotation. This relation is based on already existing table that is used across the system so I cannot make changes to the DB at all. So we have 2 tables User(Id, ShortName) and UserLink(ParentId, ChildId).
The annotation of ID is assigned to ShortName, but the actual keys in both User and UserLink are ID from User and ParentId and ChildId from UserLink.
I am trying to handle this the following way from the User entity:
#Id
#Column(name = "ShortName")
private String shortName;
#Column(name = "Id")
private long id;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name = "UserLink",
joinColumns = { #JoinColumn(name = "ParentId", referencedColumnName = "Id") },
inverseJoinColumns = { #JoinColumn(name = "ChildId", referencedColumnName = "Id") })
private Collection<UserEntity> children;
Since the key in the User entity is on the ShortName field, I have to specify the "Id" as referenced column name param. If I don't do that, it takes the ShortName as the key and doesn't fetch any data.
When I try to do this the way I showed above, I get the following exception:
Caused by: org.hibernate.MappingException: Duplicate property mapping of **_entity_UserEntity_children found in **.entity.UserEntity
at org.hibernate.mapping.PersistentClass.checkPropertyDuplication(PersistentClass.java:486)
at org.hibernate.mapping.PersistentClass.validate(PersistentClass.java:476)
at org.hibernate.mapping.RootClass.validate(RootClass.java:268)
at org.hibernate.cfg.Configuration.validate(Configuration.java:1287)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1729)
at org.hibernate.ejb.EntityManagerFactoryImpl.<init>(EntityManagerFactoryImpl.java:84)
at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:904)
... 81 more
Do you have any idea how this could be fixed? One idea is that I could change the #Id in the entity and move it to the Id property that is used for joins, but this would need a lot of effort to rewrite bad existing code.
Anyway, is it possible to make a self-join manytomany on columns that are not keys?
Cheers
Adam
I am writing a simple application using Spring and JPA. I have 2 entities: User and Role, with a relation of N..1.
Whenever I try to get any of this entities from the database, I get an exception (It's shown below). Said exception is thrown when one entity tries to get the other entity through its foreign key.
For example, when I ask for a Role, all its attributes are obtained correctly, excepting userCollection (the group of users assigned to that role).
The exception is always thrown, no matter which method I use to ask the entity (.find(pk), .createNamedQuery(), .createQuery(), ...).
Following is the code. I have skipped the non relevant parts:
User entity:
#Entity
#Table(name = "users")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Basic(optional = false)
#Size(min = 1, max = 50)
private String id;
#JoinColumn(name = "rol", referencedColumnName = "id")
#ManyToOne(optional = false, fetch = FetchType.EAGER)
private Role rol;
...
Role entity:
#Entity
#Table(name = "roles")
public class Role implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#NotNull
#Size(min = 1, max = 50)
private String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "rol", fetch = FetchType.LAZY)
private Collection userCollection;
...
The exception thrown when I try to obtain a role:
Exception [EclipseLink-6094] (Eclipse Persistence Services - 2.0.1.v20100213-r6600): org.eclipse.persistence.exceptions.QueryException
Exception Description: The parameter name [id] in the query's selection criteria does not match any parameter name defined in the query.
Query: ReadAllQuery(name="userCollection" referenceClass=User sql="SELECT ID, EMAIL, NAME, rol FROM users WHERE (rol = ?)")
at org.eclipse.persistence.exceptions.QueryException.parameterNameMismatch(QueryException.java:1031)
at org.eclipse.persistence.internal.expressions.ParameterExpression.getValue(ParameterExpression.java:246)
at org.eclipse.persistence.internal.databaseaccess.DatabaseCall.translate(DatabaseCall.java:918)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:204)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:191)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeSelectCall(DatasourceCallQueryMechanism.java:262)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.selectAllRows(DatasourceCallQueryMechanism.java:618)
at org.eclipse.persistence.internal.queries.ExpressionQueryMechanism.selectAllRowsFromTable(ExpressionQueryMechanism.java:2537)
at org.eclipse.persistence.internal.queries.ExpressionQueryMechanism.selectAllRows(ExpressionQueryMechanism.java:2496)
at org.eclipse.persistence.queries.ReadAllQuery.executeObjectLevelReadQuery(ReadAllQuery.java:455)
at org.eclipse.persistence.queries.ObjectLevelReadQuery.executeDatabaseQuery(ObjectLevelReadQuery.java:997)
at org.eclipse.persistence.queries.DatabaseQuery.execute(DatabaseQuery.java:675)
at org.eclipse.persistence.queries.ObjectLevelReadQuery.execute(ObjectLevelReadQuery.java:958)
at org.eclipse.persistence.queries.ReadAllQuery.execute(ReadAllQuery.java:432)
...
I tried to remove the userCollection from the Role entity. If I try to get a role it works just fine, but if I try to find an user, I get the following exception:
org.springframework.transaction.UnexpectedRollbackException: JTA transaction unexpectedly rolled back (maybe due to a timeout); nested exception is javax.transaction.RollbackException: Transaction marked for rollback.
at org.springframework.transaction.jta.JtaTransactionManager.doCommit(JtaTransactionManager.java:1014)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:755)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:724)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:475)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:270)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
...
I don't know how to solve this issue. I've spent a lot of time. Help would be appreciated :)
First of all, you don't indicate your table layout, which makes answering your question a guessing game.
To begin, I would avoid using #Basic on the ID column in User. #Size is ok, but you might want to match that in JPA as well. See below (the nullable=false and unique=true attributes are redundant with the #Id annotation):
#Id
#Column(length=50, nullable=false, unique=true)
#Size(min = 1, max = 50)
private String id;
If the ID is basically a user name, this is a bad idea. Things can get tricky down the road if the user wants to change his user name. Additionally, foreign key references to the users table require 50 bytes instead of 4 (it's a bit more complex than that, but you get the idea). I'd add a regular Integer ID and a separate userName field. The user never needs to see the ID.
Similar treatment should be given to Role.name:
#Column(length=50, nullable=false, unique=true)
#Size(min = 1, max = 50)
private String name;
Second, it looks like a user can have only one role? Seriously? Anyway, if that's the case the referencedColumnName attribute is not needed, since the ID column in roles uses the default name:
#ManyToOne(optional = false, fetch = FetchType.EAGER)
#JoinColumn(name = "rol")
private Role rol;
Having foreign key column named "rol" instead of "role_id" is not a good idea. I'd use the default (just drop the #JoinColumn stuff).
If you intended that users could have more than one role, that requires a join table (you can omit the #JoinTable stuff if you want to use the defaults):
#ManyToMany
#JoinTable(
name = "user_roles",
joinColumns = { #JoinColumn(name = "user_id") },
inverseJoinColumns = { #JoinColumn(name = "role_id") }
)
private List<Role> roles;
Third, it's hard to see the usefulness of having the list of users in Role. I'd remove it entirely. Instead use something like user.getRoles.contains(SomeRole) to see if a user has a certain role.
Finally, looking at your code, there are a couple of things that I should clear up:
In the "column" annotations (#Column, #JoinColumn, etc), the name and referencedColumnName attributes allow you to specify the actual column names. They do not refer to field names in your classes.
I have User class and Country class with respective tables. Country table data is fixed.
I make a mapping table User_Country(userid, countryid) and following mapping in User class
#OneToMany(fetch=FetchType.EAGER)
#JoinTable(name = "User_Country", joinColumns ={
#JoinColumn(name = "userid")
}, inverseJoinColumns = {
#JoinColumn(name = "COUNTRYID")
})
private Set<Country> country;
When i persist User class it successfully persist user data and insert data in mapping table(user_country). This is exactly i want but when i find User by using hql('from user where userid=?') and then try to get country maaping (which is stored in mapping tableuser_country). I didn't get any data from user_country. How can i write annotation so that it gets data from user_country. If i put cascade then it update country table(which is fixed) which i don't want.
I am not too sure but, try with inverse="false", as that might help.