How to sort the self-reference structure? - java

In my Spring Boot application, there is the model "Item" which has sefl-reference. I would like to realize sorting by field sortOrder, but I can't understand how to implement it for CRUD operations.
For instance, how to get the value for sortOrder before saving my entity to the repository and how to recalculate sortOrder after updating the entity. Any help will be highly appreciated! Thanks!
#Data
#Entity
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "item")
public class Item {
#Id
#JsonFormat(shape = JsonFormat.Shape.STRING)
#SequenceGenerator(name = "itemSeq", sequenceName = "item_id_seq", allocationSize = 50)
#GeneratedValue(strategy = GenerationType.IDENTITY, generator = "itemSeq")
private Long id;
#NotBlank
#Column(name = "name")
private String name;
#Column(name = "description")
private String description;
#Column(name = "sort_order")
private String sortOrder;
#Column(name = "is_active")
private Boolean isActive;
#ManyToOne
#JoinColumn(name = "parent_id")
#JsonFormat(shape = JsonFormat.Shape.STRING)
#JsonIgnoreProperties({"name", "description", "sortOrder", "isActive", "parent", "children"})
private Item parent;
#OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#Where(clause = "is_active = true")
#Fetch(FetchMode.SUBSELECT)
#OrderBy("sort_order ASC")
private List<Item> children = new ArrayList<>();
}

Suppose the new entity you want to save has a sort_order of x,
First you will have to increment all the other columns which has sort_order >= x. The easiest way you can do it is by using a native query.
#Modifying
#Query("update item set sort_order = sort_order + 1 where sort_order >= ?1", nativeQuery = true)
int incrementSortOrders(int currentSortOrder);
After this, its just a matter of saving your new entity using
repository.save(item)
Update:
If the sort_order is something of the format 1-3-0, 1-3-1, 1-3-2, 1-3-3 to indicate a recursive order, Then this is one possible solution -
Suppose we get the new entity with sort key 1-3-1,
we can still identify the items to be incremented with the > operator as they will be lexicographically larger.
We can calculate the prefix of the current node sequnce as 1-3- by writing the logic in code
Then we can run the following query to increment every other row.
#Modifying
#Query("update item set sort_order = CONCAT(:prefix ,CONVERT(SUBSTRING_INDEX(sort_order, :prefix, -1), UNSIGNED INTEGER) + 1) where sort_order > :currentNodeOrder", nativeQuery = true)
int incrementSortOrders(String prefix, String currentNodeOrder); //prefix is `1-3-` which was pre calculated and current node order is `1-3-1`

Related

Avoid updating entities with JPA based on timestamp column

let's consider two JPA entities A and B :
#Entity
#Table(name = "A")
#SequenceGenerator(name = "seq_a", allocationSize = 50, initialValue = 1)
public class A {
#Id
#GeneratedValue(generator = "seq_a", strategy = GenerationType.SEQUENCE)
#Column(name = "ID", insertable = false, updatable = false, unique = true, nullable = false)
private Long id;
#Column(name = "CODE")
private String code;
#OneToMany(mappedBy = "a", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private Set<B> bSet = new HashSet<>();
#Column(name = "CREATED_TIME")
private LocalDateTime createdTime;
//getters + setters
}
#Entity
#Table(name = "B")
#SequenceGenerator(name = "seq_b", allocationSize = 50, initialValue = 1)
public class B {
#Id
#GeneratedValue(generator = "seq_b", strategy = GenerationType.SEQUENCE)
#Column(name = "ID", insertable = false, updatable = false, unique = true, nullable = false)
private Long id;
#Column(name = "SOMETHING")
private String something;
#ManyToOne(optional = false)
#JoinColumn(name = "A_ID", nullable = false, updatable = false)
private A a;
#Column(name = "CREATED_TIME")
private LocalDateTime createdTime;
//getters + setters
}
then consider RestController (springboot context) that have one GET method used for retrieving detail of entity A :
#GetMapping("/{id}")
public ResponseEntity<ADTO> getA(#PathVariable(name = "id", required = true) Long id) {
return aRepository.findById(id)
.map(a -> new ResponseEntity<>(mapper.mapToDomain(a), HttpStatus.OK))
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
method POST used for creating records of A:
#PostMapping
public ResponseEntity<ADTO> addA(#RequestBody #Valid ADTO aDTO) {
return new ResponseEntity<>(mapper.mapToDomain(a.save(mapper.mapToEntity(ADTO))), HttpStatus.CREATED);
}
and PUT method for updating :
#PutMapping
public ResponseEntity<ADTO> updateA(#RequestBody #Valid ADTO aDTO) {
A a = aRepository.findById(aDTO.getId()).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
ADTO aDTOfound = mapper.mapToDomain(a);
BeanUtils.copyProperties(aDTO, aDTOfound);
return new ResponseEntity<>(mapper.mapToDomain(aRepository.save(mapper.mapToEntity(aDTOfound), HttpStatus.OK)));
}
then let's say that, createdTime attribute is updated everytime the entity is persisted (including created - updating createdTime attribute is done under the hood ...).
Then let's consider scenario, where two users at the same time are retrieving detail of the same entity A (id 1). If user X update the detail from the retrieved content via PUT method, is there any way how to avoid user Y to update the same entity with old content (notice that the createdTime attribute is updated on record with id 1) ? I know that one possible solution, is to make sure that the retrieved createdTime and one from aDTO in update method is the same, but is there any "more" standard solution for this problem ? For example, how to avoid updating entity A (if it was updated previously with USER 1) but let update the childs in Bset which ones for example were not updated by user 1 ...
This is typical problem statement of Optimistic Locking
Optimistic locking is a mechanism that prevents an application from
being affected by the "lost update" phenomenon in a concurrent
environment while allowing some high degree of concurrency at the same
time.
I will solve this problem using #Version, add #Version field in your entity like below
#Entity
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Column(name = "student_name")
private String studentName;
#Column(name = "roll_number")
private String rollNumber;
#Column(name = "version")
#Version
private Long version;
}
In above case When we create an entity for the first time default version value will be zero
On update, the field annotated with #Version will be incremented and subsequent update will same version will fail with OptimisticLockException
The #Version annotation in hibernate is used for Optimistic locking while performing update operation. For more details you can visit https://vladmihalcea.com/jpa-entity-version-property-hibernate/

Hibernate: Filter result list of entities by values of inner list

I'm trying to implement multi-tenancy.
Currently i have following entities:
#Data
#Entity
public class Zone {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "[NAME]")
private String name;
}
And Student entity:
#Data
#Entity
#Table(name = "[Student]")
#FilterDef(name = "zoneFilter", parameters = {#ParamDef(name = "zoneValue", type = "string")})
#Filter(name = "zoneFilter", condition = "zones.zone_id = :zoneValue")
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "FIRST_NAME")
private String firstName;
#Column(name = "AGE")
private Integer age;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER)
#JoinTable(name = "student_zone", joinColumns = #JoinColumn(name = "student_id"),
inverseJoinColumns = #JoinColumn(name = "zone_id"))
private List<Zone> zones;
}
So, i would like to get only that Students which have corresponding zoneValue in zones list. With current implementation Hibernate generates following query:
select student0_.id as id1_0_, student0_.age as age2_0_, student0_.first_name as first_na3_0_, student0_.tenant_id as tenant_i4_0_ from [student] student0_ where zones.zone_id = ?
So, it throws exception that zones.zone_id couldn't be bound.
So, how could i filter Students by values of the zones? Is it possible to do that by using Hibernate Filter?
Probably, i have to put proper type in FilterDef, but when i write Zone Hibernate throws exception that it couldn't determine such type.
Have you tried to add
#FilterJoinTable(name="zoneFilter", condition=":zoneValue = zone_id")
private List<Zone> zones;
and remove
#Filter(name = "zoneFilter", condition = "zones.zone_id = :zoneValue")
(because I think the filter should be on the property and not the class)
then call
Filter filter = session.enableFilter("randomName");
filter.setParameter("zoneFilter", new String("myZoneValue"));
in order to active the filter?
I think Hibernate doesn't know the zones.value because you don't use an alias (http://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#pc-filter-sql-fragment-alias)
This will give us something like that:
#Filter(
name="zoneFilter",
condition="{z}.zone_id = :zoneValue",
aliases = {
#SqlFragmentAlias( alias = "z", table= "student_zone"),
}
)

SQL Query Too Complex To Express In JPA Criteria API?

I have an SQL query that gets me exactly the data I need. The problem is that we are trying to express all queries in JPA Criteria API to maintain portability, and I can't figure out how to map this particular query.
The problem is that the JPA Criteria API Subquery class lacks the multiselect() method that CriteriaQuery class has. As you can see in the SQL query, I have computed fields in the sub-query which don't exist in the entity. Thus, I have no way to retrieve these fields.
I would be quite appreciative if anyone knows a solution or could offer guidance, or even if someone could validate that what I am trying to achieve in JPA Criteria API is not possible.
The SQL:
SELECT w.NAME AS 'wave_name',
Count(*) AS 'num_lines',
Sum(qty_ordered) AS 'num_units',
Count(DISTINCT unit_of_work_id) AS 'num_units_of_work',
Sum(completed_units) AS 'completed_units',
( Sum(completed_units) + Sum(qty_scratched) ) / Sum(qty_ordered) AS 'perc_completed_units'
FROM (SELECT t.id,
t.wave_id,
t.quantity_requested AS 'qty_ordered',
t.quantity_scratched AS 'qty_scratched',
t.unit_of_work_id AS 'unit_of_work_id',
Ifnull(m.quantity, 0) AS 'qty_picked',
CASE
WHEN Ifnull(m.quantity, 0) > quantity_requested THEN
quantity_requested
ELSE Ifnull(m.quantity, 0)
END AS 'completed_units'
FROM task t
LEFT OUTER JOIN (SELECT move.task_id,
Sum(quantity) AS 'quantity'
FROM move
GROUP BY task_id) m
ON m.task_id = t.id) s
JOIN wave w
ON w.id = s.wave_id
GROUP BY w.name;
The entities:
#Entity
#Table(name = "task")
public class Task {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
private Long id;
#ManyToOne (cascade = CascadeType.ALL)
#JoinColumn (name = "wave_id", nullable = false)
private Wave wave;
#ManyToOne (cascade = CascadeType.ALL)
#JoinColumn (name = "unit_of_work_id", nullable = false)
private UnitOfWork unitOfWork;
#OneToMany (cascade = CascadeType.ALL, mappedBy = "task")
private Set<Move> moves = new HashSet<Move>();
#Column (name = "quantity_requested")
private Long quantityRequested;
#Column (name = "quantity_scratched")
private Long quantityScratched;
}
#Entity
#Table(name = "wave")
public class Wave {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "wave", cascade = CascadeType.ALL)
private Set<Task> tasks = new HashSet<Task>();
}
#Entity
#Table(name = "unit_of_work")
public class UnitOfWork {
#Id
#GeneratedValue (strategy = GenerationType.IDENTITY)
#Column (name = "id")
private Long id;
#OneToMany(mappedBy = "unitOfWork", cascade = CascadeType.ALL)
private Set<Task> tasks = new HashSet<Task>();
}
#Entity
#Table(name = "move")
public class Move {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
private Long id;
#ManyToOne (cascade = CascadeType.ALL)
#JoinColumn (name = "task_id", nullable = false)
private Task task;
#Column (name = "quantity")
private Long quantity;
}
I would say use named parameters or native query approach for this.
For example:
Named parameters:
public interface Repo extends JpaRepository<AEntity, String> {
#Query("select a from AEntity a where a.BEntity.name = :name")
public aMethod( #Param("name") String name)
}
OR
Native query approach:
public interface Repo extends JpaRepository<AEntity, String> {
#Query(value = "select * from Tablename t where t.name = :name", nativeQuery=true)
public aMethod(#Param("name") String name)
}
Check this link if you are using spring jpa
http://docs.spring.io/spring-data/data-jpa/docs/1.4.x/reference/htmlsingle/#jpa.named-parameters

Many-To-Many extra columns Spring JPA

Has passed a week and I'm struggling with a problem and it seems that I'm not able to find any answer to it.
I have this structure:
Album model:
#Entity
#Table(name = DatabaseConstants.ALBUM_TABLE_NAME)
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class Album {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 100)
private String name;
#Column(nullable = false)
private int imageVersion = 1;
#Column(length = 255)
private String description;
#Column(nullable = false)
private boolean single = false;
#Column(nullable = false)
private Long createdAt;
#Column(nullable = true)
private Long deletedAt;
// Relations
#OneToMany(fetch = FetchType.LAZY)
private List<AlbumView> albumViews;
// Getters and Setters
}
AlbumView model:
#Entity
#Table(name = DatabaseConstants.ALBUM_VIEW_RELATION_TABLE_NAME)
public class AlbumView {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private boolean bigger;
#Column(nullable = false)
private int position;
#Column(nullable = false)
private Long createdAt;
#Column(nullable = true)
private Long deletedAt;
// Relations
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "album_id")
private Album album;
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "view_id")
private View view;
// Getters and Setters
}
View model:
#Entity
#Table(name = DatabaseConstants.VIEW_TABLE_NAME)
public class View {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private String name;
#Column(nullable = false)
private Long createdAt;
#Column(nullable = true)
private Long deletedAt;
// Relations
#OneToMany(fetch = FetchType.LAZY)
private List<AlbumView> albumViewList;
// Getters and Setters
}
I need to search a list of albums by an view. I that the query that I need is something like (Using #Query annotation from Spring JPA):
SELECT a, av.bigger, av.position FROM Album a, AlbumView av WHERE av.view.id = ?
But I can't map those two values (bigger, position) because they aren't on the Album model. I need to build an response like:
[
{
id: 1,
name: 'Test Album',
imageVersion: 1,
description: 'Description one',
single: true,
bigger: true,
position: 1
},
{
id: 2,
name: 'Test Album 2',
imageVersion: 1,
description: 'Description two',
single: true,
bigger: false,
position: 2
}
]
As far as I read, I can't use #Transient to help me here (because apparently JPA ignores it and the #Query don't fill those attributes) and I don't know other way to do it.
Thanks.
EDIT 1
I tried the #bonifacio suggestion using the following code in the Repository class of Spring JPA:
#Query("SELECT av.album.id, av.album.name, av.album.imageVersion, av.bigger, av.position FROM AlbumView av WHERE av.view.id = ?1")
List<AlbumView> findByViewId(Long id);
But I got the following response:
[
[
1,
"Test Best Hits",
1,
true,
1
]
]
The values is exactly that, but it is considering that is an array, and not an object like its supposed..
One way to do this is by changing the query to select the AlbumView entity, which contains all the desired fields from both AlbumView and Album entity, and also can use the View entity in the WHERE clause of your query.
In this case, one of the possible solutions would be using the following query:
SELECT av FROM AlbumView av WHERE av.view.id = ?1
Also, I want to note that in your first example you were trying to fetch three different objects per row, like the example bellow:
SELECT a, av.bigger, av.position FROM Album a, AlbumView av WHERE av.view.id = ?
The reason that it does not work is because when you select more than one column (or object), you are not automatically fetching the desired fields in just one line, since Hibernate will convert the result in a Object array (Object[]) for each line of your query's result. So, unless you really need to select multiple objects in one row, it's always recommended to return only one field, or entity on each SELECT you make.

Hibernate querying

I'm sorry for my maybe foolish question. I have products and orders tables (with many-to -many relationship), also i have an user table. And now I want to get the product count by user_id and by special field "order_satus". I can make two queries get order by special criteria and then get size of product in order. But this is not optimal at all. When i use JDBCTemplate I did a lot of joins and get only one query.
Here are my entities:
#Entity
#Table(name = "shop.order")
public class Order {
#Id
#Column(name = "order_id")
#GeneratedValue(strategy = GenerationType.AUTO)
private long orderId;
private long user_id;
#Column(name = "databegin")
private Date dateBegin;
#Column(name = "dataend")
private Date dateEnd;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "order_product", joinColumns = { #JoinColumn(name = "order_id") }, inverseJoinColumns = { #JoinColumn(name = "product_id") })
private List<Product> products;
}
Product Entity
#Entity
#Table(name = "product")
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int product_id;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "order_product", joinColumns = { #JoinColumn(name = "product_id") }, inverseJoinColumns = { #JoinColumn(name = "order_id") })
private List<Order> order;
public List<Order> getOrder() {
return order;
}
public void setOrder(List<Order> order) {
this.order = order;
}
#Column
#NotBlank
private String name;
#Column
#Max(value = 250)
private String descr;
#Column
#Max(value = 250)
private String manufacturer;
#Column
private Double price;
#Column
private Byte[] barcode;
#Column
private Byte[] picture;
#ForeignKey(name = "category_id")
private int category_id;
As you gave sql query..
'select count(*) from produtc p join order ord on ord.id = ? and
ord.status = ?' – Igor Masternoy
And according to the Entity structure you gave, the HQL will be..
select ord.products productList from Order ord where ord.id=? and ord.status=?
This query will return you list of products (List<Product> products) and then you can get the count by java code i.e. productList.size(); This size is the product count you need based on order id and order status you will pass as parameter and also you can append user.id in where cause to filter your result as per user.
This is productList as per your need..
Query query = getSession().createQuery("select ord.products productList from Order ord where ord.id=:orderID and ord.status=:orderStatus");
query.setInteger("orderID", orderIDParameter);
query.setString("orderStatus", orderStatusParameter);
List<Product> productList = (List<Product>) query.list();
Integer productCount = productList.size();
This productCount is your product count you need.
If I get it right, you can have many orders for one user, and many products for one order.
I think a good option is to use a DetachedCriteria and to build your query with it.
Should look like (not tested):
DetachedCriteria userCriteria = DetachedCriteria.forClass(User.class);
userCriteria.add(Projections.groupProperty("user_id"));
userCriteria.add(Projections.count("product_id");
DetachedCriteria orderCriteria = userCriteria.createCriteria("order.user_id","order",CriteriaSpecification.LEFT_JOIN);
DetachedCriteria orderCriteria = orderCriteria.createCriteria("order_product.order_id","product",CriteriaSpecification.LEFT_JOIN);
//orderCriteria.add(Restrictions.eq(...); // I can't see a "status" field in your model
List results = userCriteria.list();
select size(ord.products) from Order ord where ord.id = :orderId and ord.status = :orderStatus
or
select count(*) from Order ord join ord.products where ord.id = :orderId and ord.status = :orderStatus
The first form will result in a subquery to get the count. The second relies on the join to create the sql product over which to apply the count. The first form is more intuitive, in my opinion, while the second query will perform better in most cases.

Categories

Resources