I am trying to write the following query outlined here on sqlfiddle in JPA. I first tried using the #query annotation with native = true and that does work, but my issue is that I want the query to be more dynamic, because it could be the case where I don't want to add the clause to filter by name or by account.
My entities look something like this:
#Entity
#Table(name = "INSTRUCTION")
public class Instruction {
#Id
#Column(name = "ID", nullable = false, unique = true)
public Long id;
#Column(name = "ACCOUNT", nullable = false)
public String account;
#Column(name = "NAME", nullable = false)
public String name;
#OneToMany(cascade = CascadeType.All, fetch = FetchType.EAGER)
#JoinColumn(name = "INSTRUCTION_ID", referenceColumnName = "ID")
#OrderBy("lastUpdated")
private List<Audit> auditItems = new ArrayList<>();
//Getters & Setters
}
.
#Entity
#Table(name = "AUDIT")
public class Audit {
#Id
#Column(name = "ID", nullable = false, unique = true)
public Long id;
#Column(name = "INSTRUCTION_STATUS", nullable = false)
public InstructionStatus status;
#Column(name = "LAST_UPDATED", nullable = false)
public LocalDateTime lastUpdated;
#Column(name = "LAST_UPDATED_BY", nullable = false)
public String lastUpdatedBy;
//Getters & Setters
}
I had looked into using specifications to do this, and I managed to break my query into different specifications like so:
private Specification<Instruction> hasAccount(String account) {
return (root, query, criteriaBuilder) -> criteriaBuilder.in(root.get("account")).value(account);
}
private Specification<Instruction> havingStatus(List<String> status) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
final Subquery<Audit> auditSubquery = query.subquery(Audit.class);
final Root<Audit> audit = auditSubquery.from(Audit.class);
//select instruction id from audit where status is not in {status}
auditSubquery.select(audit.get("instruction").get("id"));
auditSubquery.where(criteriaBuilder.trim(audit.get("status")).in(status).not());
//Select instruction from table where
predicates.add(root.get("id").in(auditSubquery).not());
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
};
}
// Other specifications....
And these work fine when called like so:
final List<Instruction> instructions = this.instructionRepository.findAll(
where(havingStatus(statuses)
.and(hasAccount(account))));
But my goal is have it so that for example I could check if account == null then do not include the hasAccount specification, and so on for other fields that may be null. Is there a way I can do this?
This should do the trick.
Specification spec = where(null);
if (statuses != null) {
spec = spec.and(havingStatus(statuses))
}
if (account != null) {
spec = spec.and(hasAccount(account))
}
final List<Instruction> instructions = this.instructionRepository.findAll(spec);
Related
Im learning, and so far i created many to many bidirectional database - user can create many groups and group can have many users - and i cannot find a way for my GroupsController Post mapping to work, from my understanding, it requires to get firstly Users id, in order to set the right relationship in Join table for Group, because the relationship should be set only when user create/join group, not when user create sign up procedure. Postman throws 500 and intelliJ:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.ilze.highlight.entity.Groups.getId()" is null] with root cause
java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.ilze.highlight.entity.Groups.getId()" is null
I use lombok - #Data, #Getter, therefore getId() should be available for use from Group class. My GroupsController with POST mapping when user decides to create a new group:
#RestController
#RequestMapping("api/groups") // pre-path
public class GroupsController{
#Autowired
private GroupsService groupsService;
#Autowired
private UserService userService;
#Autowired
private final GroupsRepository groupsRepository;
#Autowired
private UserRepository userRepository;
public GroupsController(GroupsRepository groupsRepository) {
this.groupsRepository = groupsRepository;
}
#GetMapping("/all-groups")
public List<Groups> getGroups(){
return (List<Groups>) groupsRepository.findAll();
}
#PostMapping("/user/{usersId}/create-group")
public ResponseEntity<Groups> createGroup(#PathVariable(value = "usersId") Long usersId, #RequestBody Groups groupRequest){
Groups group = userRepository.findById(usersId).map(users -> {
long groupsId = groupRequest.getId();
// add and create new group
users.addGroup(groupRequest);
return groupsRepository.save(groupRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found user with id = " + usersId));
return new ResponseEntity<>(group, HttpStatus.CREATED);
}
}
Group database class:
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Getter
#Table(name = "group_collection")
public class Groups {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name ="group_name", unique = true, nullable = false, length = 20)
private String groupName;
#Column(name = "size", nullable = false)
private int size;
#Column(name = "strict", nullable = false)
private boolean strict;
#Column(name = "open", nullable = false)
private boolean open;
#Column(name ="description", length = 300)
private String description;
#Column(name = "create_time", nullable = false)
private LocalDateTime createTime;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.DETACH,
CascadeType.REFRESH
},
mappedBy = "groups")
#JsonIgnore
private Set<User> users = new HashSet<>();
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
}
And Users class for database:
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "username", unique = true, nullable = false, length = 100)
private String username;
#Column(name = "password", nullable = false)
private String password;
#Column(name = "email", nullable = false)
private String email;
#Column(name = "create_time", nullable = false)
private LocalDateTime createTime;
#Enumerated(EnumType.STRING)
#Column(name = "role", nullable = false)
private Role role;
#Transient
private String accessToken;
#Transient
private String refreshToken;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.DETACH,
CascadeType.REFRESH
})
#JoinTable(name = "groups_x_user",
joinColumns = { #JoinColumn(name = "users_id") },
inverseJoinColumns = {#JoinColumn(name = "groups_id")})
private Set<Groups> groups = new HashSet<>();
public void addGroup(Groups group) {
this.groups.add(group);
group.getUsers().add(this);
}
public void removeGroup(long id){
Groups group = this.groups.stream().filter(g ->
g.getId() == id).findFirst().orElse(null);
if(group != null){
this.groups.remove(group);
group.getUsers().remove(this);
}
}
For reference my GroupsService implementation:
#Service
public class GroupsServiceImpl implements GroupsService{
private final GroupsRepository groupsRepository;
public GroupsServiceImpl(GroupsRepository groupsRepository) {
this.groupsRepository = groupsRepository;
}
#Override
public Groups saveGroup(Groups group) {
group.setCreateTime(LocalDateTime.now());
return groupsRepository.save(group);
}
#Override
public Optional<Groups> findByGroupName(String groupName) {
return groupsRepository.findByGroupName(groupName);
}
}
You need to persist the object from request. And since you have Many-2-Many relation, you can insert related object from both sides. In your case: just add existing user to the newly created group
The method will look something like that:
#PostMapping("/user/{usersId}/groups")
public ResponseEntity<Groups> createGroup(#PathVariable(value = "usersId") Long usersId, #RequestBody Groups groupRequest) {
Groups createdGroup = userRepository.findById(usersId)
.map(user -> {
groupRequest.setId(null); // ID for new entry will be generated by entity framework, prevent override from outside
groupRequest.getUsers().add(user); // add relation
return groupsRepository.save(groupRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found user with id = " + usersId));
return new ResponseEntity<>(createdGroup, HttpStatus.CREATED);
}
I have a little problem to understand how it make sense to insert a List of languages in JPA for a Oracle 19 DB.
The API receives a DTO with a list of chosen languages for one user.
{
"userId": 173125,
"logname": "testor2",
"lastname": "Testimator2",
"firstname": "Testor2",
"selectedLang": [
"German",
"English"
],
}
The Table looks like this for User and had a Table with pk and fk for user and language code.
The Table should remove unused languages that not in the received DTO || insert new languages in the table that not exist.
Tables that used in the db are:
USER
Columnname
Type
keys
USERID
int
PK
LOGNAME
row
-
NAME
varchar
-
LASTNAME
varchar
-
USERLANGUAGE
Columnname
Type
keys
USERLANGUAGEID
int
PK --part of primary key
USERID
int
PK --part of primary key
Languagecode
varchar
-
Now the tricky part. I want to store the languages as a List and have thought that jpa deals with the update, remove or inserts itself.
#Entity
#Table(name = "user", schema = "test")
public class UserEntity {
#Id
#Column(name = "USERID", nullable = false)
private long userid;
#Column(name = "LOGNAME", nullable = false, length = 50)
private String logname;
#Column(name = "NAME", nullable = false, length = 60)
private String name;
#Column(name = "LASTNAME", nullable = false, length = 80)
private String lastname;
#OneToMany(mappedBy = "userid", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserlanguageEntity> languages;
public List<UserlanguageEntity> getLanguages() {
return languages;
}
public void setLanguages(List<UserlanguageEntity> languages) {
this.languages = languages;
}
public UserEntity addLanguage(UserlanguageEntity userlanguageEntity) {
languages.add(userlanguageEntity);
userlanguageEntity.setUserid(this);
return this;
}
public UserEntity removeLanguage(UserlanguageEntity userlanguageEntity) {
languages.remove(userlanguageEntity);
userlanguageEntity.setUserid(null);
return this;
}
}
UserLanguageEntity
#Entity
#Table(name = "USRLANGUAGE")
public class UsrlanguageEntity {
#EmbeddedId
private UserId user;
private String languagecode;
#Basic
#Column(name = "LANGUAGECODE", nullable = true, length = 4)
public String getLanguagecode() {
return languagecode;
}
public void setLanguagecode(String languagecode) {
this.languagecode = languagecode;
}
public UserId getUser() {
return user;
}
public void setUser(UserId user) {
this.user = user;
}
...
}
Embedded ID
#Embeddable
public class UserId implements Serializable {
#SequenceGenerator(name = "userlanguageid", sequenceName =
"USERLANGUAGEID", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"userlanguageid")
#Column(name = "USERLANGUAGEID", nullable = false, precision = 0)
private long userlanguageid;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "USERID")
private UserEntity userid;
...
}
So the fetch method is fine and works as intended, but when i want to update the languages I get errors.
First of all get the list out of db to an UserEntity, then want to clear the UserLanguageEntitylist -> the list in the UserEntity. And than add the received list, maybe thats imposible. So what would be the best implementation to get this to work. First i want to delete all languagecodes in the db and write the new list into it, but it feels a bit dirty to me because maybe some have not to be changed.
public void setUserDataForCompany(UserDto userDto) {
CompanyEntity companyEntity = companyRepository.findById(41).orElseThrow(
NoSuchElementException::new);
UserEntity userEntity = userRepository.findById(userDto.getUserId()).orElseThrow(
NoSuchElementException::new);
companyUserDataMapper.toEntity(userDto, companyEntity, userEntity);
setLanguagesForUser(userDto, userEntity);
userRepository.save(userEntity);
}
// Here i have no clue to update existing or store missing languages
private void setLanguagesForUser(UserDto userDto, UserEntity userEntity) {
userDto.getSelectedLang().forEach(language -> {
UserlanguageEntity userlanguageEntity = new UserlanguageEntity();
userlanguageEntity.setLanguagecode(language.getLngCode());
userEntity.addLanguage(userlanguageEntity);
});
}
Could someone get me a hint to doing it the right way?
Thanks a lot and cheers
I got two entity class one:
#Table(name = "package")
public class Package {
#Id
#Column(name = "package_id", insertable = false, nullable = false)
private Long packageId;
#Column(name = "timestamp", nullable = false, updatable = false)
#Temporal(TemporalType.TIMESTAMP)
private Date timestamp;
#Column(name = "name", nullable = false)
private String name;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "queue_id",foreignKey=#ForeignKey(name = "package_queue_id_fk"))
private Queue Queue;
#Column(name = "file_number", nullable = true)
private Integer fileNumber;
And
#Table(name = "queue")
public class Queue {
#Id
#Column(name = "queue_id", insertable = false, nullable = false)
private Integer queue;
#Column(name = "description", nullable = true)
private String description;
#Column(name = "name", nullable = false)
private String name;
#OneToMany(mappedBy = "Queue", fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REFRESH })
#MapKeyColumn(name = "package_id")
private Set<Package> packages;
And I would like to find List of fileNumbers depending on package.name and package.queue.queue_id
So currently I got only one condition (name) and it looks like this:
public List<Integer> getAllFileNumbers(String fileName, Integer queueId) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Integer> query = cb.createQuery(Integer.class);
Root<Package> package = query.from(package.class);
query.select(package.get("fileNumber")).where(cb.equal(package.get("name"), fileName));
return em.createQuery(query).getResultList();
}
Anyone could help me add another one, on top of thet remamber that the value need to be from another entity.
Edit:
So after #Leviand hint I did it like this:
Predicate filenamePred= cb.equal(package.get("name"), fileName);
Predicate queueIdPred = cb.equal(package.get("queue_id"), queueId);
query.select(package.get("fileNumber")).where(cb.and(filenamePred, queueIdPred ));
I got error:
Caused by: java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [queue_id] on this ManagedType
You need to add the two predicates ( you can ofc write it all inline, I'm splitting so it's clearer) in a or condition, for example:
Predicate filename= cb.equal(package.get("name"), fileName);
Predicate queueId = cb.equal(package.get("queue"), queueId);
//then use them in a or condition
query.select(root).where(cb.and(filename, queueId ));
queue_id is the name of the column. You have to use the name of the field, which is Queue, and you have to get it's id field (queue) to compare.
This can be made easier and more type-safe if you use the metamodel-generator.
I am trying to understand and figure out the solution for the following use case
These are my entity classes
User
#Entity
#Table(name = "USER")
public class User {
private UserID id;
private Set<UserAddress> addresses = new HashSet<UserAddress>(0);
#EmbeddedId
#AttributeOverrides( {
#AttributeOverride(name = "userId", column = #Column(name = "USER_ID", nullable = false, length = 32)),
#AttributeOverride(name = "userType", column = #Column(name = "USER_TYPE", nullable = false, precision = 12, scale = 0)) })
public User getId() {
return this.id;
}
#OneToMany(fetch = FetchType.EAGER, mappedBy = "user", cascade={CascadeType.ALL})
#BatchSize(size=50)
public Set<UserAddress> getAddresses() {
return this.addresses;
}
........
}
UserAddress
#Entity
#Table(name = "USERADDRESS")
public class UserAddress {
private UserID id;
Private User user;
private String address;
#EmbeddedId
#AttributeOverrides( {
#AttributeOverride(name = "userId", column = #Column(name = "USER_ID", nullable = false, length = 32)),
#AttributeOverride(name = "userType", column = #Column(name = "USER_TYPE", nullable = false, precision = 12, scale = 0)) })
public User getId() {
return this.id;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumns( {
#JoinColumn(name = "userId", referencedColumnName = "USER_ID", nullable = false, insertable=false, updatable=false),
#JoinColumn(name = "userType", referencedColumnName = "USER_TYPE", nullable = false, insertable=false, updatable=false) })
public User getUser() {
return this.user;
}
........
}
UserId
#Embeddable
public class UserId implements Serializable {
private String userNo;
private Long UserType;
.......
.......
}
I have created a staticmetamodel class for User, UserID and UserAddress and created query based on Specifications.
Metamodel class for User
#StaticMetamodel(User.class)
public abstract class User_ {
public static volatile SetAttribute<User, UserAddress> addresses;
public static volatile SingularAttribute<User, UserID> id;
}
Metamodel for UserId
#StaticMetamodel(UserID.class)
public abstract class UserID_ {
public static volatile SingularAttribute<UserID, String> userNo;
public static volatile SingularAttribute<UserID, Long> UserType;
}
I am trying to retrieve maximum of 10 User objects ordered by UserType and searched based on userId. The query has to retrieve the UserAddresses as an eager fetch.
My Specification Object is
UserSpecification
public class UserSpecifications {
public static Specification<User> userNoIs(String userNo) {
return (root, query, cb) -> {
root.fetch(User_.addresses);
return cb.equal(root.get(User_.id).get(UserID_.userNo),userNo);
};
}
}
DAO Function:
Sort sortInstructions = new Sort(Sort.Direction.DESC, "id.userNo");
Pageable pageInfo = new PageRequest(0, maxCount, sortInstructions);
Specifications<User> specifications = Specifications.where(userNoIs(input.getUserNo()));
Page<User> responseList= userRepository.findAll(specifications,pageInfo);
I am getting the following exception when I run the above statement.
Caused by: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=generatedAlias1,role=com.entity.User.addresses,tableName=USERADDRESS ,tableAlias=useraddress1_,origin=USER user0,columns={user0.USER_TYPE user0.USER_ID ,className=com.entity.UserAddress}}]
But apart from that I also need to understand how to limit the no of rows returned using Specification but without using Pageable . If I use Pageable, then a separate query will be fired to retrieve the count of rows and then actual query is being fired. My application is performance oriented, and I do not want to have any extra queries being fired.
Is there any way where I can limit the no of rows without using Pageable, but using Specifications ?
[MVC, Servlets + JSP, JPA, MySQL]
I am working on simple Blog application. I am using JPA to map entities to MySQL tables. Here is code excerpt from entities in question:
Entity Post:
#NamedQueries({
#NamedQuery(name = "getNewestPosts", query = "SELECT p FROM Post p ORDER BY p.date DESC"), // getting resultList ordered by date
#NamedQuery(name = "getMostVisitedPosts", query = "SELECT p FROM Post p ORDER BY p.visitors DESC") // ordered by most visited
})
#Entity
#Table(name = "post")
public class Post implements Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "post_id", unique = true, nullable = false)
private Integer id;
#Column(name = "post_title", length=300, unique = false, nullable = false)
private String title;
#Column(name = "post_date", unique = false, nullable = false)
private Date date;
#Column(name = "post_summary", length=1000, unique = false, nullable = true)
private String summary;
#Column(name = "post_content", length=50000, unique = false, nullable = false)
private String content;
#Column(name = "post_visitors", unique = false, nullable = false)
private Integer visitors;
#OneToMany(cascade = { ALL }, fetch = LAZY, mappedBy = "post")
private Set<Comment> comments = new HashSet<Comment>();
...
Entity Comment:
#Entity
#Table(name = "comment")
public class Comment implements Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "comment_id", unique = true, nullable = false)
private Integer id;
#Column(name = "comment_title", length=300, unique = false, nullable = false)
private String title;
#Column(name = "comment_date", unique = false, nullable = false)
private Date date;
#Column(name = "comment_content", length=600, unique = false, nullable = false)
private String content;
#ManyToOne
#JoinColumn (name = "post_id", referencedColumnName="post_id", nullable = false)
private Post post; ...
Blog home page should contain summaries of 10 newest posts. So, in PostDAO object I have defined next method (returns all posts from db ordered by date):
public List<Post> getNewestPosts(){
Query q = em.createNamedQuery("getNewestPosts");
List<Post> resultList = (List<Post>) q.getResultList();
if (resultList.isEmpty())
return null;
else
return resultList;
}
I would like to implement pagination in some simple way, probably passing certain request parameters and reading data in jsp using jstl (i'm not yet familiar with jquery). Now, how to approach to implementing pagination in MVC? Which parameters I need to be attaching to request? How should I approach to implementing page navigation links (previous, page numbers, next) in JSP?
I think you can use the setMaxResults and the setFirstResult methods on the namedQuery. Keep passing the First result as a function of the number of records to be displayed on the page and the page number.
If you use Spring MVC there is already a way to do it and you can take a look at the PageListHolder api documentation. I havent used this but i have stumbled upon the API.