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?
Related
Say I have at least two entities.
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true)
private String name;
#ManyToAny(
metaColumn = #Column(name = "node_type"),
fetch = FetchType.LAZY
)
#AnyMetaDef(
idType = "long", metaType = "string",
metaValues = {
#MetaValue(targetEntity = Milestone.class, value = MILESTONE_DISC),
#MetaValue(targetEntity = Phase.class, value = PHASE_DISC)
}
)
#Cascade({org.hibernate.annotations.CascadeType.ALL})
#JoinTable(
name = "process_nodes",
joinColumns = #JoinColumn(name = "process_id", nullable = false),
inverseJoinColumns = #JoinColumn(name = "node_id", nullable = false)
)
private Collection<ProcessNode> nodes = new ArrayList<>();
...
}
#Entity
#ToString
#DiscriminatorValue(MILESTONE_DISC)
public class Milestone implements ProcessNode {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Collection<ResultDefinition> results;
#ToString.Exclude
#ManyToOne(fetch = FetchType.LAZY)
#Transient
private Process process;
...
}
Now I want to use spring data jpa specification to find (all) processes which have a milestone with name "S5".
Note that Milestone is a ProcessNode and there is another Entity called Phase which is also a ProcessNode. These can be contained in the "nodes" collection of my Process Entity.
I tried to write something like this:
public static Specification<Process> hasMilestoneWithName(final String milestoneName) {
return (Specification<Process>) (root, query, criteriaBuilder) -> {
Path<?> namePath = root.join("nodes").get("name");
return criteriaBuilder.equal(namePath, milestoneName);
};
}
This does not work, but throws:
Caused by: java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [nodes] on this ManagedType [com.smatrics.dffs.processservice.model.entities.Process]
I don't really know how to use the API. Examples often refer to a meta-model that would be generated by the IDE or maven, but I really do not want to have any static generated resources. Please help me resolve this with Specification of spring-data-jpa without a generated meta-model.
Also if you could help me write the hql it would be awesome.
Thanks!
I would suggest a simpler alternative, coming from bottom-up:
Load Milestone entities with name=S5: findByName("S5")
Return the Process for each Milestone
Filter out the duplicates
Or you could even save a few SQL queries by returning not the Milestone entity but only the ID of the Process for each Milestone and then load the Process nodes by a list of IDs:
The (native) SQL equivalent would be
select *
from process
where id in (
select process_id
from milestone
where name = 'S5'
)
Regardless of my solution your join does not look completely correct to me but I can't point out what's wrong - maybe there are other methods on the JPA metamodel that return a CollectionJoin? Not sure. Probably it is because #ManyToAny is not JPA standard so the JPA criteria API does not recognize nodes as a valid "joinable" field.
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
Scenario :
I have 3 tables, Offer, Channel and Offer_Channels.
Basically Channel is a lookup table, i.e, the values in that table can neither be inserted nor updated by the application. An offer can contain one or many channels. I use the Channel table values to populate dynamic checkboxes. Anyways, so here is what I have :
#Entity
#Table(name = "OFFER")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Offer implements Serializable {
// Offer Id
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "offer_seq_gen")
#Column(name = "OFFER_ID")
private long OfferId;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "OFFER_CHANNELS", joinColumns = { #JoinColumn(name = "OFFER_ID") }, inverseJoinColumns = { #JoinColumn(name = "CHANNEL_ID") })
private Set<Channel> channels = new HashSet<Channel>();
//Other fields and corresponding getters and setters
}
Here is the Channel entity :
#Entity
#Table(name = "CHANNEL")
public class Channel implements Serializable {
private static final long serialVersionUID = 1L;
#NotNull
#Id
#Column(name = "CHANNEL_ID", insertable=false, updatable=false)
private Long channelId;
#Column(name = "CHANNEL_NAME", insertable=false, updatable=false)
private String channelName;
//getters and setters
}
Now, when a user creates an offer, I need to insert row in Offer table and Offer_Channels tables and do nothing(No updates/inserts) for Channel table. Initially, all three would happen, so to achive the "do nothing to Channel table" part, I put insertable=false and updateable=false on the Channel table columns and that worked like a charm. Now I used plain Hibernates for this. I mean I wrote a standalone java application and a main class to add an offer useing hibernate's session.save(offer). It ran the following queries :
Hibernate: insert into OFFER
Hibernate: insert into OFFER_CHANNELS
Alright, now, I have a rest service where I am using the Spring's JPA repository to save the information and I have the same domain objects setup. Now, when I add an offer, it runs :
Hibernate: insert into OFFER
Hibernate: insert into CHANNEL ( It is failing here obviously. I don't want this step to happen)
My question :
1. Why is it is trying to write something to Channel table even though I gave insertable=false in its domain object, and this is only happening with the Spring JPA setup. With the hibernate setup it just works fine.
3. Doesn't #JoinTable/ #OneToMany / insertable / updateble , go well with Spring JPA repository ?
What am I missing here ?
UPDATE :
#Service
#Transactional
public class OfferService {
#Inject
private OfferRepository offerRepository;
public Offer saveOfferInformation(Offer offer) {
log.debug("Saving Offer Info..");
log.debug("Offer object :"+offer);
return offerRepository.save(offer);
}
}
Repo :
public interface OfferRepository extends JpaRepository<Offer, Long> {
List<Offer> findByBuySku(String buySku);
}
And in the REST service Im just injecting the service and calling it, so no business logic in the REST service. Right now Im getting and the reason is it is trying to insert record to Channel table:
exception: "org.springframework.dao.DataIntegrityViolationException"
message: "could not execute statement; SQL [n/a]; constraint [PVS_OWNER.CHANNEL_PK]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement"
Have you tried to add insertable and updatable on the #JoinColumn. This works with One to Many relationships. I'm not 100% sure if it works with a Many to Many relationship.
#JoinTable(name = "OFFER_CHANNELS", joinColumns = { #JoinColumn(name = "OFFER_ID", insertable = false, updatable = false ) }, inverseJoinColumns = { #JoinColumn(name = "CHANNEL_ID", insertable = false, updatable = false ) })
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
There are two tables with #OneToMany and #ManyToOne bidirectional relation, like this:
#Entity
public class Asset {
private int id;
private int count;
#OneToMany
private Set<Dealing> dealings;
...
}
#Entity
public class Dealing {
private int id;
...
#ManyToOne
#JoinColumn(name = "customer_id", nullable = false, updatable = false)
private Customer customer;
#ManyToOne
#JoinColumn(name = "product_id", nullable = false, updatable = false)
private Product product;
#ManyToOne(cascade = CascadeType.ALL)
private Asset asset;
}
all things sound OK, but when I want to search data using Restriction like this,
session.createCriteria(Asset.class).add(Restrictions.eq("dealings.customer.id", customerId)).add(Restrictions.eq("dealing.product.id", productId)).list();
In this level I get this error,
could not resolve property: dealings.customer of: com.project.foo.model.Asset
one of the solutions are to change my strategy but i wasted time to find this,btw I don't have any idea about it, do you ?
First of all, you don't have a bidirectional OneToMany association, but two unrelated unidirectional associations. In a bidirectional OneToMany association the One side must be marked as the inverse of the Many side using the mappedBy attribute:
#OneToMany(mappedBy = "asset")
private Set<Dealing> dealings;
Second, using the criteria API for such static queries is overkill, and leads to code that is harder to read than necessary.I would simply use HQL which is much easier to read. Criteria should be used for dynamic queries, IMHO, but not for static ones:
select asset from Asset asset
inner join asset.dealings dealing
where dealing.customer.id = :customerId
and dealing.product.id = :productId
Whether you use HQL or Criteria, you can't use asset.dealings.customer, since asset.dealings is a collection. A collection doesn't have a customer attribute. To be able to reference properties from the Dealing entity, you need a join, as shown in the above HQL query. And it's the same for Criteria:
Criteria criteria = session.createCriteria(Asset.class, "asset");
criteria.createAlias("asset.dealings", "dealing"); // that's an inner join
criteria.add(Restrictions.eq("dealing.customer.id", customerId);
criteria.add(Restrictions.eq("dealing.product.id", productId);