Criteria API in Spring Boot - java

I have the following (simplified) entity structure:
public class Animal {
private long id;
private int regionId;
private int categoryId;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "animal")
private List<Attributes> attributes;
}
public class Attributes {
private long id;
#ManyToOne(fetch = FetchType.LAZY)
private Animal animal;
private String name;
private String value;
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Detail detail;
}
public class Detail {
private long id;
private String size;
private String type;
}
This is part of a Spring Boot application.
What I would like to achieve is to query Animal by both their own attributes and by attributes in Details.
My query needs to look like this:
GET: /animal/{regionId}/{categoryId}?size=medium,large&type=carnivorous,herbivore
This would mean that I need to request all animals that have a certain regionId and categoryId and that also have Details with size and type within the value list provided. Also - and I think this is the tricky part - the size and type parameters are optional, so the query needs to take that into account.
Currently I have a PlanReposiory that extends CrudRepository and provides basic query methods for the Plan entity.
I was trying to wrap my head around the Criteria API to figure out a way to use it to achieve this goal, but I don't understand how I can put all that in my repository. Any ideas?

You should have a look at Spring Data JPA Specifications:
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications
You have to extend your Repository from JpaSpecificationExecutor
public interface CustomerRepository
extends CrudRepository<Customer, Long>, JpaSpecificationExecutor {
…
}
And then you the a findAll method that takes Specification paramter:
List<T> findAll(Specification<T> spec);
Then you can create your specification based on the parameters passed in the URL:
public static Specification<Animal> bySizeAndType(String size, String type) {
return new Specification<Animal>() {
public Predicate toPredicate(Root<Animal> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
// JOIN Attributes
// JOIN Detail
if (size != null) {
// add condition
}
if (type != null) {
// add condition
}
return builder.where(...);
}
};
}
I hope this helps.

Related

JPA: Adding new entry to a many-to-many taking long time

im having a problem when adding a new entry in a many-to-many relationship because the list is huge. Ex:
Item item = new Item(1);
Category cat = dao.find(1, Category.class);
List<Category> list = new ArrayList<>();
list.add(cat);
item.setCategoryList(list);
cat.getItemList().add(item);
The problem is that the Category Itens list is huge, with a lot of itens, so performing the cat.getItemList() takes a very long time. Everywhere i look for the correct way to add a many-to-many entry says that a need to do that. Can someone help?
Edit:
A little context: I organize my itens with tags, so 1 item can have multiple tags and 1 tag can have multiple itens, the time has pass and now i have tags with a lot of itens ( > 5.000), and now when i save a new item with one of thoses tags it takes a long time, i have debuged my code and found that most of the delay is in the cat.getItensList() line, with makes sense since it has a extensive list o itens. I have searched a lot for how to do this, and everyone says that the correct way to save a entry in a many-to-many case is to add to the list on both sides of the relationship, but if one side is huge, it will takes a lot of time since calling the getItensList() loads them in the context. Im looking for a way to save my item refering the tag witout loading all of the itens of that tag.
Edit 2:
My classes:
Item:
#Entity
#Table(name = "transacao")
#XmlRootElement
public class Transacao implements Serializable {
#ManyToMany(mappedBy = "transacaoList")
private List<Tagtransacao> tagtransacaoList;
...(other stuff)
}
Tag:
#Entity
#Table(name = "tagtransacao")
#XmlRootElement
public class Tagtransacao implements Serializable {
#JoinTable(name = "transacao_has_tagtransacao", joinColumns = {
#JoinColumn(name = "tagtransacao_idTagTransacao", referencedColumnName = "idTagTransacao")}, inverseJoinColumns = {
#JoinColumn(name = "transacao_idTransacao", referencedColumnName = "idTransacao")})
#ManyToMany
private List<Transacao> transacaoList;
...(other stuff)
}
Edit 3:
WHAT I DID TO SOLVE:
As answered by Ariel Kohan, i tried to do a NativeQuery to insert the relationship:
Query query = queryDAO.criarNativeQuery("INSERT INTO " + config.getNomeBanco() + ".`transacao_has_tagtransacao` "
+ "(`transacao_idTransacao`, `tagtransacao_idTagTransacao`) VALUES (:idTransacao, :idTag);");
query.setParameter("idTransacao", transacao.getIdTransacao());
query.setParameter("idTag", tag.getIdTagTransacao());
I was able to reduce the time of que query from 10s to 300milis what it is impressive. In the end its better for my project that it is already runnig to do that instead of creating a new class that represents the many-to-many reletionship. Thanks to everyone who tried to help \o/
In this case, I would prevent your code from load the item list in memory.
To do that, I can think about two options:
Using a #Modyfing query to insert the items directly in the DB.
[Recommended for cases where you want to avoid changing your model]
You can try to create the query using normal JPQL but, depending on your model, you may need to use a native query. Using native query would be something like this:
#Query(value = "insert into ...", nativeQuery = true)
void addItemToCategory(#Param("param1") Long param1, ...);
After creating this query, you will need to update your code removing the parts where you load the objects in memory and adding the parts to call the insert statements.
[Update]
As you mentioned in a comment, doing this improved your performance from 10s to 300milis.
Modify your Entities in order to replace #ManyToMany with #OneToManys relationship
The idea in this solution is to replace a ManyToMany relationship between entities A and B with an intermediate entity RelationAB. I think you can do this in two ways:
Save only the Ids from A and B in RelationAB as a composite key (of course you can add other fields like a Date or whatever you want).
Add an auto-generated Id to RelationAB and add A and B as other fields in the RelationAB entity.
I did an example using the first option (you will see that the classes are not public, this is just because I decided to do it in a single file for the sake of simplicity. Of course, you can do it in multiple files and with public classes if you want):
Entities A and B:
#Entity
class EntityA {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
public EntityA() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
#Entity
class EntityB {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
public EntityB() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
RelationABEntity and RelationABId:
#Embeddable
class RelationABId implements Serializable {
private Long entityAId;
private Long entityBId;
public RelationABId() {
}
public RelationABId(Long entityAId, Long entityBId) {
this.entityAId = entityAId;
this.entityBId = entityBId;
}
public Long getEntityAId() {
return entityAId;
}
public void setEntityAId(Long entityAId) {
this.entityAId = entityAId;
}
public Long getEntityBId() {
return entityBId;
}
public void setEntityBId(Long entityBId) {
this.entityBId = entityBId;
}
}
#Entity
class RelationABEntity {
#EmbeddedId
private RelationABId id;
public RelationABEntity() {
}
public RelationABEntity(Long entityAId, Long entityBId) {
this.id = new RelationABId(entityAId, entityBId);
}
public RelationABId getId() {
return id;
}
public void setId(RelationABId id) {
this.id = id;
}
}
My Repositories:
#Repository
interface RelationABEntityRepository extends JpaRepository<RelationABEntity, RelationABId> {
}
#Repository
interface ARepository extends JpaRepository<EntityA, Long> {
}
#Repository
interface BRepository extends JpaRepository<EntityB, Long> {
}
A test:
#RunWith(SpringRunner.class)
#DataJpaTest
public class DemoApplicationTest {
#Autowired RelationABEntityRepository relationABEntityRepository;
#Autowired ARepository aRepository;
#Autowired BRepository bRepository;
#Test
public void test(){
EntityA a = new EntityA();
a = aRepository.save(a);
EntityB b = new EntityB();
b = bRepository.save(b);
//Entities A and B in the DB at this point
RelationABId relationABID = new RelationABId(a.getId(), b.getId());
final boolean relationshipExist = relationABEntityRepository.existsById(relationABID);
assertFalse(relationshipExist);
if(! relationshipExist){
RelationABEntity relation = new RelationABEntity(a.getId(), b.getId());
relationABEntityRepository.save(relation);
}
final boolean relationshipExitNow = relationABEntityRepository.existsById(relationABID);
assertTrue(relationshipExitNow);
/**
* As you can see, modifying your model you can create relationships without loading big list and without complex queries.
*/
}
}
The code above explains another way to handle this kind of things. Of course, you can make modifications according to what you exactly need.
Hope this helps :)
This is basically copied from a similar answer I gave earlier but similar question as well. The code below ran when I first write it but I changed the names to match this question so there might be some typos. The spring-data-jpa is a layer on top of JPA. Each entity has its own repository and you have to deal with that. For dealing with the many-to-many relations specifically in spring-data-jpa you can make a separate repository for the link table if you think it's a good idea.
#Entity
public class Item {
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ItemCategory> categories;
#Entity
public class Category {
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ItemCategory> items;
#Entity
public class ItemCategory {
#EmbeddedId
private ItemcategoryId id = new ItemcategoryId();
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("itemId")
private Item Item;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("categoryId")
private Category category;
public ItemCategory() {}
public ItemCategory(Item Item, Category category) {
this.item = item;
this.category = category;
}
#SuppressWarnings("serial")
#Embeddable
public class ItemCategoryId implements Serializable {
private Long itemId;
private Long categoryId;
#Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ItemCategoryId that = (ItemCategoryId) o;
return Objects.equals(itemId, that.itemId) && Objects.equals(categoryId, that.categoryId);
}
#Override
public int hashCode() {
return Objects.hash(itemId, categoryId);
}
And to use it. Step 3 shows the way you are currently doing it and creates a read of the existing joins before doing the update. Step 4 just inserts a relation directly in the join table and does not cause a pre-read of the existing joins.
#Transactional
private void update() {
System.out.println("Step 1");
Category category1 = new Category();
Item item1 = new Item();
ItemCategory i1c1 = new ItemCategory(Item1, Category1);
categoryRepo.save(Category1);
ItemRepo.save(Item1);
ItemCategoryRepo.save(p1t1);
System.out.println("Step 2");
Category category2 = new Category();
Item item2 = new Item();
ItemCategory p2t2 = new ItemCategory(item2, category2);
ItemRepo.save(item2);
categoryRepo.save(category2);
ItemCategoryRepo.save(p2t2);
System.out.println("Step 3");
category2 = CategoryRepo.getOneWithitems(2L);
category2.getitems().add(new ItemCategory(item1, category2));
categoryRepo.save(Category2);
System.out.println("Step 4 -- better");
ItemCategory i2c1 = new ItemCategory(item2, category1);
itemCategoryRepo.save(i2c1);
}
I don't explicitly set the ItemCategoryId id's. These are handled by the persistence layer (hibernate in this case).
Note also that you can update ItemCategory entries either explicity with its own repo or by adding and removing them from the list since CascadeType.ALL is set, as shown. The problem with using the CascadeType.ALL for spring-data-jpa is that even though you prefetch the join table entities spring-data-jpa will do it again anyway. Trying to update the relationship through the CascadeType.ALL for new entities is problematic.
Without the CascadeType neither the items or categories lists (which should be Sets) are the owners of the relationship so adding to them wouldn't accomplish anything in terms of persistence and would be for query results only.
When reading the ItemCategory relationships you need to specifically fetch them since you don't have FetchType.EAGER. The problem with FetchType.EAGER is the overhead if you don't want the joins and also if you put it on both Category and Item then you will create a recursive fetch that gets all categories and items for any query.
#Query("select c from Category c left outer join fetch c.items is left outer join fetch is.Item where t.id = :id")
Category getOneWithItems(#Param("id") Long id);

Spring Data Projection size()

Is there a way to return the size of a collection via rest api projection?
Consider this example:
The data:
#Entity
#Table
public class MyData {
// id
// ...
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "mydata")
private final Set<User> users = new HashSet<>();
// getters/setters ...
}
the repository:
#RepositoryRestResource
public interface MyDataRepository extends PagingAndSortingRepository<MyData, Long> {
}
the projection:
#Projection(name = "short", types = {MyData.class})
public interface MyDataProjection {
// neither of those work
// #Value("#{target.getUsers().size()}")
#Value("#{target.users.size()}")
Integer nrUsers();
}
I want to get the number of Users in a MyData-Object returned via REST api.
For example: my-domain/my-service/mydatas/123/?projection=short
should return:
{
"nrUsers": 4;
...
}
Is it possible anyway?
The naming convention ist to start with a "get" since the attributes of the projection are methods, not fields. So this works:
#Value("#{target.users.size()}")
Integer getNrUsers();
(instead of the previous "nrUsers()")

JPA - How to query using Specification and #EmbeddedId?

I'm using a JPA query that uses a specification to retrieve entities. When I execute the query, I'm getting the error:
org.springframework.data.mapping.PropertyReferenceException: No property name found for type Task!
I've looked at the answers to similar questions that have been asked on this site previously & tried to model my code to follow the patterns that were recommended but the code is still failing.
When I step through the code with a debugger, the expanded path in the criteria builder is returning the embedded ID class, but when the specification is actually used in the query it looks like the attribute is being applied to the base entity class.
Am I missing something obvious?
Here is the entity class:
#Entity
#Table(name = "TASKS")
public class Task implements Serializable {
#EmbeddedId
private TaskId id;
...more attributes, getters and setters
}
Here is the embedded ID entity class:
#Embeddable
public class TaskId implements Serializable {
#Column(name = "NAME", length = 100)
private String name;
...more attributes, getters and setters
}
Here is the specification builder that matches on the embedded id 'name' attribute:
public class HasTaskNameSpec {
private HasTaskNameSpec() {
}
public static Specification<Task> equals(String name) {
return (root, query, criteriaBuilder) -> {
return criteriaBuilder.equal(root.get("id").get("name"), taskName);
};
}
}
The query is executed on the repository as follows:
List<Task> results = taskRepository.findAll(HasTaskNameSpec.equals("foo"));
The repository itself is very simple:
public interface TaskRepository extends JpaRepository<Task, TaskId>, JpaSpecificationExecutor<Task> {
List<Task> findByIdName(String name);
Page<Task> findByIdName(String name, Pageable page);
}
** EDIT added methods to repository as was suggested below **
Ahh, the root cause was totally in our codebase. There was a sort order being specified on the page that didn't include the embedded "id" attribute. The above code works.
'root.get({embeddedIdName}).get({subPropertyName})' is used to query on embeddedId using specification.
#Embeddable
public class ProjectId implements Serializable{
private static final long serialVersionUID = 1L;
#Column(name = "PROJECT_NAME")
private String projectName;
#Column(name = "ORGANIZATION")
private String organization;
......
......
}
#Entity
#Table(name = "projects")
public class Project {
#EmbeddedId
private ProjectId projectId;
#Column(name = "STARTED_TIME")
private Timestamp startedTime;
#Column(name = "ACTIVE")
private String active;
#Column(name = "DESCRIPTION")
private String description;
......
......
}
In the above snippet, ProjectId is an embedded id. To query on projectName, we should use below snippet.
expression = root.get("projectId").get("projectName");
Demo application link.
Take a look at this link which has a similar query.
EmbbededId Lookup
The final answer suggests that you can add a method to your TaskRepository thus.
public interface TaskRepository extends JpaRepository<Task, TaskId>, JpaSpecificationExecutor<Task> {
public List<Task> findByIdName(String name);
}

Direct self-reference leading to cycle Superclass issue JSON

I have tried several things I found while searching but nothing helped or I did not implement it correctly.
Error I'm getting
Direct self-reference leading to cycle (through reference chain: io.test.entity.bone.Special["appInstance"]->io.test.entity.platform.ApplicationInstance["appInstance"])
Both these extend the base entity and in the base (super class) it has an appInstance as well.
Base entity looks similar to this
#MappedSuperclass
public abstract class BaseEntity implements Comparable, Serializable {
#ManyToOne
protected ApplicationInstance appInstance;
//getter & setter
}
Application entity looks like this
public class ApplicationInstance extends BaseEntity implements Serializable {
private List<User> users;
// some other properties (would all have the same base and application instance . User entity will look similar to the Special.)
}
Special entity
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "objectType")
#JsonIgnoreProperties({"createdBy", "appInstance", "lastUpdatedBy"})
public class Special extends BaseEntity implements Serializable {
#NotNull
#Column(nullable = false)
private String name;
#Column(length = Short.MAX_VALUE)
private String description;
#NotNull
#Column(nullable = false)
private Double price;
#OneToOne
private Attachment image;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = SpecialTag.class)
#CollectionTable(name = "special_tags")
#Column(name = "specialtag")
private List<SpecialTag> specialTags;
#Temporal(TemporalType.TIME)
private Date specialStartTime;
#Temporal(TemporalType.TIME)
private Date specialEndTime;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = WeekDay.class)
#CollectionTable(name = "available_week_days")
#Column(name = "weekday")
private List<WeekDay> availableWeekDays;
#OneToMany(mappedBy = "special", cascade = CascadeType.REFRESH)
private List<SpecialStatus> statuses;
#OneToMany(mappedBy = "special", cascade = CascadeType.REFRESH)
private List<SpecialReview> specialReviews;
#Transient
private Integer viewed;
private Boolean launched;
#OneToMany(mappedBy = "special")
private List<CampaignSpecial> specialCampaigns;
#Override
#JsonIgnore
public ApplicationInstance getAppInstance() {
return super.getAppInstance();
}
}
All entities in Special inherits from BaseEntity which contains AppInstance
then i have a method to get the special
#GET
#Path("{ref}")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(value = MediaType.TEXT_PLAIN)
public Special findByGuestRef(#PathParam("ref") String pRefeference) {
// find the special and return it
return special;
}
On the special entity I tried the following
Added jsonIgnoreProperties
Added an override for appInstance to annotate with #JsonIgnore
#JsonIdentityInfo
links for the above
https://stackoverflow.com/a/29632358/4712391
Jackson serialization: how to ignore superclass properties
jackson self reference leading to cycle
none of those solutions works. Am I doing something wrong?
Note: Would it also just be possible to edit special, since the other entities are in a different package and would not like to edit them.
Usually excluding attributes in a response is as easy as adding a #JsonIgnore annotation to their getters, but if you don't want to add this annotation to a parent class, you could override the getter and then add the annotation on it:
public class Special extends BaseEntity implements Serializable {
...
#JsonIgnore
public ApplicationInstance getAppInstance() {
return this.appInstance;
}
...
}
NOTE: As there are several frameworks, make sure that you are using the correct #JsonIgnore annotation or it will be ignored, see this answer for instance.
Another option, more "manual", is just creating a bean for the response which would be a subset of the Special instance:
#GET
#Path("{ref}")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(value = MediaType.TEXT_PLAIN)
public SpecialDTO findByGuestRef(#PathParam("ref") String pRefeference) {
// find the special and return it
return new SpecialDTO(special);
}
public class SpecialDTO {
//declare here only the attributes that you want in your response
public SpecialDTO(Special sp) {
this.attr=sp.attr; // populate the needed attributes
}
}
To me, problem seems to be in the Special object and the fields being initialized in it.
I guess that there is a circular reference detected when serialisation happens.
Something similar to:
class A {
public A child;
public A parent;
}
A object = new A();
A root = new A();
root.child = object;
object.parent = root;
In the above code, whenever you will try to seralize either of these objects, you will face the same problem.
Note that public fields are not recommended.
I'll suggest to peek into your Special object and the references set in it.

Join generic type with JPA and Criteria API

i'm trying to map the following entity:
I have an Order entity that contains different types of OrderData(can also be shared between multiple Order entities), depending on the actual order(different produckts, product variants etc):
//Simplified example
#Entity
#IdClass(OrderPK.class)
#Table(name = "tablename"
public class Order<T extends OrderData> {
#Id
#ManyToOne
#JoinColumn(name = "whatever")
private T orderData;
// Might be complicating stuff
#Id
#ManyToOne
#JoinColumn(name = "somecolumn")
private Account buyerAccount;
// ...
}
// OrderData base class
#Entity
#Table(name = "thatothertable"
#Inheritance(strategy=InheritanceType.JOINED)
public class OrderData {
#Id
#Column(name = "id")
private Long id;
// extra stuff
}
Now the problem is: how can i get this to join the right subclass of OrderData?
I want to be able to write something like this:
List<Order<CustomOrderData>> ordersWithCustomOrderData = this.orderDAO.findAllOrders();
and get all Order entities with CustomOrderDatas.
Is there a way to achieve this?
So far my DAO codes looks like this:
public List<Order> findAllOrders() {
CriteriaBuilder cb = this.em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> root = cq.from(Order.class):
cq.select(root):
return this.em.createQuery(cq).getResultList();
}
How would i have to change the DAO code to support the generic type?
Is it actually possible? If not, is there any alternative design to achieve a comparable structure and functionability (Orders with different OrderDatas, ability to search for Orders with specific subclass of OrderData)?
// Not working pseudo code of what i want
public <T extends OrderData> List<Order<T>> findAllOrders() {
CriteriaBuilder cb = this.em.getCriteriaBuilder();
// With createQuery(Order.class) i will get CrtieriaQuery<Order>
CriteriaQuery<Order<T>> cq = cb.createQuery(Order.class);
// as above
Root<Order<T>> root = cq.from(Order.class):
cq.select(root):
return this.em.createQuery(cq).getResultList();
}
1. Idea
Why do you need a generic Attribute? Generic entities are not possible in JPA but you could just put the reference to a Orderdata as:
private OrderData orderData;
2. Idea
You can save this enum in your entity and then compare OrderDataType.getClazz() to your OrderData1.getClass() and you can query for this types. If you are using JPA 2.1 you can save the full classname in the database column with a custom Fieldconverter and then query for the class.
public enum OrderDataType {
Order1(Order1.class);
private Class<? extends OrderData> clazz;
private OrderDataType(Clazz<? extends OrderData> clazz) {
this.clazz = clazz;
}
public Class<? extends OrderData> getClazz() {
return clazz;
}
}

Categories

Resources