Override Pageable findAll for selecting fewer columns in Spring Data Rest - java

How to Override spring data repository to select only selected columns when going to pages that are discovered from /api page in spring data rest.
I added findAll as below -
public interface UserRepository extends BaseRepository<User, Integer>, UserRepositoryCustom {
#Query("select u from User u where email = :email and password = :password")
#Cacheable(value = "user-cache", key = "#user.login")
#RestResource(exported = false)
public User findUserByEmailAndPassword(#Param("email") String email, #Param("password") String password);
#RestResource(rel = "byEmail", path = "byEmail")
public User findUserByEmail(#Param("email") String email);
#RestResource(rel = "byPhone", path = "byPhone")
public User findUserByPhone(#Param("phone") String phone);
#Override
#Query("select u.id,u.email,u.phone from User u ")
public Page<User> findAll(Pageable pageable);
}
/api/users is giving an error -
{"cause":null,"message":"PersistentEntity must not be null!"}

I created a UserSummaryProjection class in same package as User
#Projection(name = "summary", types = User.class)
public interface UserSummaryProjection {
Integer getId();
String getEmail();
}
Then, going at /api/users or /users/3?projection=summary gives me desired result without changing the Repository.

Selecting subelements of User and still creating a User is somewhat counterintuitive.
I would create another entity for example UserDetails, that will be mapped by the same table with the same mapping.
public class UserDetails {
private int uid;
private String email;
private String phone;
}
And create a Repository, based on this new Entity.

Related

how to have a GET request made to "/users?<some-query-here>", then all users that meet the query criteria are returned in a JSON array

Long-time reader, first-time poster.
I need help. I can't figure out URL parameters ↔ DataBase.
Situation:
I put .../users/?role=admin&title=manager into postman
Expected:
a json of all the users who are managers and admins.
Actual:
My computer blows up.
#RestController
#RequestMapping(path = USERS_PATH)
#Log4j2
public class UserController
// other code...
#GetMapping
public ResponseEntity<List<User>> getUserQuery(
#RequestParam( required = false, name = "name") String name,
#RequestParam( required = false, name = "title") String title,
#RequestParam( required = false, name = "roles") String roles,
#RequestParam( required = false, name = "email") String email,
#RequestParam( required = false, name = "password") String password
) {
log.info("Request received for getUserQuery");
return new ResponseEntity<>(userService.doSomething???(), HttpStatus.OK); // stuff I don't understand yet)
}
My Question:
After the controller, what goes into the UserService and UserRepository?
Extra info:
I'm using Spring Boot and H2 but may need to swap to PostgreSQL DB later.
I have the code below as a check for unique emails during puts and posts on the service layer, but I couldn't get something similar to work for this issue.
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
#Query("SELECT u FROM User u WHERE u.email = ?1")
Optional<User> findUserByEmail(String email);
}
Rant:
This seems like a super common thing, and I should already know how to do this. I don't. I've been reading the internet for like 5 hours. Still no answer. I learned stuff about Specifications, Querydsl, #ModelAtribute, DAO, and mapping RequestParams. However, I couldn't find how to connect the dots. So, I went full caveman and tried making an #Query parameter using StringBuilder and a bunch of logic...I'm starting to spiral.
Anyway, I would like to avoid specifications & Querydsl. Just use #Query and JPA, but more importantly, I want a clean solution/ best practice.
Here is a working snip. I think there is a problem with your repository. You are using #Query and also out-of-box support for queries.
What goes in Service and repository: The repository layer(repo) classes are used to abstract interactions with the DB.
The service layer interacts with repo layer and do the massaging of the data that the repository layer returns.
UserEntity
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
#Getter
#Setter
#Entity
#Table(name = "users")
#NoArgsConstructor
public class UserEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "designation")
private String designation;
#Column(name = "email")
private String email;
}
UserRepository
import com.example.code.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
#Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByName(String name);
List<UserEntity> findByDesignation(String designation);
Optional<UserEntity> findByEmail(String email);
}
UserService
import java.util.Collection;
public interface UserService {
void createUser(UserDTO userDTO);
Collection<UserDTO> getUsers(
String username,
String designation,
String email
);
}
UserServiceImpl
import com.example.code.dto.UserDTO;
import com.example.code.entity.UserEntity;
import com.example.code.mapper.UserMapper;
import com.example.code.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.apache.catalina.User;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
#Service
#RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
#Override
public void createUser(UserDTO userDTO) {
UserEntity userEntity = new UserEntity();
userEntity.setName(userDTO.getName());
userEntity.setDesignation(userDTO.getDesignation());
userEntity.setEmail(userDTO.getEmail());
userRepository.save(userEntity);
}
#Override
public Collection<UserDTO> getUsers(String username, String designation, String email) {
Set<UserDTO> userDTOS = new HashSet<>();
if( !username.isBlank() && !username.isEmpty() && userRepository.findByName(username).isPresent() ) {
userDTOS.add(UserMapper.toDto(
userRepository.findByName(username).get()
));
}
if(!designation.isBlank() && !designation.isEmpty()) {
userDTOS.addAll(
userRepository.findByDesignation(designation)
.stream()
.map(UserMapper::toDto)
.collect(Collectors.toSet())
);
}
if( !email.isBlank() &&
!email.isEmpty() &&
userRepository.findByEmail(email).isPresent() ) {
userDTOS.add(UserMapper.toDto(
userRepository.findByEmail(email).get()
));
}
return userDTOS;
}
}
UserMapper
import com.example.code.dto.UserDTO;
import com.example.code.entity.UserEntity;
public class UserMapper {
public static UserDTO toDto(UserEntity entity) {
UserDTO userDTO = new UserDTO();
userDTO.setName(entity.getName());
userDTO.setDesignation(entity.getDesignation());
userDTO.setEmail(entity.getEmail());
return userDTO;
}
}
TestController
#RestController
#RequestMapping("/test")
#RequiredArgsConstructor
public class TestController {
private final UserService userService;
#PostMapping
public ResponseEntity<String> createUser(#RequestBody final UserDTO userDTO) {
try {
userService.createUser(userDTO);
}catch (Exception e) {
return ResponseEntity.internalServerError().body("Failure");
}
return ResponseEntity.ok("Success");
}
#GetMapping
public ResponseEntity<Collection<UserDTO>> getUsers(
#RequestParam(value = "name", required = false) String name,
#RequestParam(value = "designation", required = false) String designation,
#RequestParam(value = "email", required = false) String email
) {
return ResponseEntity.ok(userService.getUsers(name, designation, email));
}
}
A combination of SQL AND, OR clause should serve your purpose. A sample is given below using a service and repository class.
UserService
public List<User> doingSomething(String name, String title, String roles, String email,String password) {
return userRepository.detailQuery(name, title, roles, email, password);
}
UserRepository
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
#Query("SELECT u FROM User u WHERE (:name is null or u.name = :name) and (:title is null or u.title = :title) and (:roles is null or u.roles = :roles) and (:email is null or u.email = :email) and (:password is null or u.password = :password)")
List<User> detailQuery(#Param("name") String name,
#Param("title") String title,
#Param("roles") String roles,
#Param("email") String email,
#Param("password") String password);
}
The simple answer to this with having queries as individual params is with JPA's Query by Example method
So your code would look something like this:
Service:
public List<User> getByExample(User user) {
return userRepository.findAll(Example.of(user,
ExampleMatcher.matchingAny().withIgnoreCase()));
}
On the controller class, you wouldn't even have to provide the individual params anymore. You just have to provide the User as a parameter to the method. JPA is smart enough to imply the properties of the user from the params you give. So your controller class could simply look something like this instead:
Controller:
#GetMapping
public ResponseEntity<List<User>> getUserQuery(User user) {
log.info("Request received for getUserQuery");
return new ResponseEntity<>(userService.getByExample(user), HttpStatus.OK);
}
5.1.6 Query By Example, here we go:
We don't need anything in our repo, since JpaRepository "has it already on board"!
Our service would look like:
package com.example;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.ignoreCase;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.startsWith;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
#Service
public class PersonService {
#Autowired
JpaRepository<Person, Long> personRepository;
public List<Person> find(String name, String title, String roles, String email, String password) {
Person person = Person.of(name, title, roles, email, password);
ExampleMatcher matcher = ExampleMatcher
.matchingAny()
.withIgnoreNullValues()
.withMatcher("name", ignoreCase())
.withMatcher("title", ignoreCase())
.withMatcher("roles", ignoreCase())
.withMatcher("email", ignoreCase())
.withMatcher("password", exact());
return personRepository.findAll(Example.of(person, matcher));
}
} // UN-TESTED, but compilable ;)
...but we see: how much "tweaking" is possible with just these five parameters.
Usage:
...
Query by Example is well suited for several use cases:
Querying your data store with a set of static or dynamic constraints.
Frequent refactoring of the domain objects without worrying about breaking existing queries.
Working independently from the underlying data store API.
Query by Example also has several limitations:
No support for nested or grouped property constraints, such as firstname = ?0 or (firstname = ?1 and lastname = ?2).
Only supports starts/contains/ends/regex matching for strings and exact matching for other property types.
I got this working, but I don't know if it's best practices.
controller:
#GetMapping
public ResponseEntity<List<User>> getUserQuery(
#RequestParam( required = false, name = "name") String name,
#RequestParam( required = false, name = "title") String title,
#RequestParam( required = false, name = "roles") String roles,
#RequestParam( required = false, name = "email") String email,
#RequestParam( required = false, name = "password") String password
) {
log.info("Request received for getUserQuery");
return new ResponseEntity<>(userService.QueryUsers(name, title, roles, email, password), HttpStatus.OK); // stuff I don't understand yet)
}
Service:
public List<User> QueryUsers(String name, String title, String roles,String email,String password) {
if (name == null && title == null && roles == null && email == null && password == null) {
return userRepository.findAll(Sort.by(Direction.ASC, "id"));
}
//TODO: make to lower case
List<User> users = new ArrayList<>();
users.addAll(userRepository.findAllUsersByName(name));
users.addAll(userRepository.findAllUsersByTitle(title));
users.addAll(userRepository.findAllUsersByRoles(roles));
users.addAll(userRepository.findAllUsersByEmail(email));
users.addAll(userRepository.findAllUsersByPassword(password));
return users;
}
Repository:
#Query("SELECT u FROM User u WHERE u.name = ?1")
Collection<? extends User> findAllUsersByName(String name);
#Query("SELECT u FROM User u WHERE u.title = ?1")
Collection<? extends User> findAllUsersByTitle(String title);
#Query("SELECT u FROM User u WHERE u.roles = ?1")
Collection<? extends User> findAllUsersByRoles(String roles);
#Query("SELECT u FROM User u WHERE u.email = ?1")
Collection<? extends User> findAllUsersByEmail(String email);
#Query("SELECT u FROM User u WHERE u.password = ?1")
Collection<? extends User> findAllUsersByPassword(String password);
Seems to be working, but I need to test it more.

spring boot Cannot invoke Repository because Repository is null

public class UserList {
private String id;
private String email;
private String userType;
private String rolls;
private String partner;
private Integer customersLinked;
private String position;
private String status;
#Autowired
ICustomerRepository customerRepository;
public UserList (Users user){
this.id = user.getId();
this.email = user.getEmail();
this.userType = user.getUserType();
this.rolls = user.getRolls();
this.partner = user.getPartner();
List<Customer> customersLinked = customerRepository.findAllByLinkedUsersIn(user.getId());
this.customersLinked = 0;
this.position = user.getPosition();
this.status =user.getStatus();
}
//Getter and Setter
}
This class is used as a list in the frontEnd ,get specific data ,not send all the data
#RequestMapping(value = "usersLinked/{id}/{type}", method = RequestMethod.GET)
public Object getUsersLinkedById(#PathVariable("id") String id,#PathVariable("type") Integer type) {
List<String> users = null;
switch (type) {
case 0:
users = usersRepository.findAll().stream().map(m -> m.getId()).collect(Collectors.toList());
break;
}
//Add userList
List<UserList> userList = new ArrayList<>();
if(users != null)
{
users.forEach(userId ->
{
Optional<Users> user = this.usersRepository.findById(userId);
userList.add(new UserList(user.get()));
});
}
return userList;
}
}
As you can see from above I am calling al the data from the user repository and sending it the list
My customer repository
public interface ICustomerRepository extends MongoRepository<Customer, String> {
Customer findByBusinessInformation_businessName(String businessName);
List<Customer> findByBusinessInformation_partnerAssigned(String partnerAssigned);
#Query("{ 'LinkedUsers' : ?0 }")
Customer findByLinkedUsers(String id);
List<Customer> findAllByLinkedUsersIn(String id);
}
In the userList I get the error when I add the logic wityh the customerRepository ,without the repository there everything is working(Want to use the repository to get an array of customer and then get the size() of the array and add it to linkedCustomers). Am I missing sommething
You are trying to inject the field customerRepository using Autowired annotation, but your class is not injectable.
You can add an annotation #Repository on your class UserList
Or use constructor injection (better way to inject your beans)
You're probably missing the #repository annotation on top of your repository class.
Another unrelated word of advice:
In your controller you use findAll and filter in java to keep only the ids.
Then you go to the same repository and perform another query per user-id from above.
This is a causing you to create multiple database calls which are one of the most expensive operations you can do, when you already have all your data from the first single query...
Also if you're only looking at the bottom part of the function you don't event need a query per each user-id (when you have a list of user ids as input), you can create a query that uses the 'in' convention and pass a list of user-ids to create a single db call.
First of all I would get rid of #Autowired ICustomerRepository customerRepository; in UserList class. It doesn't belong there. The counting of linked customers should be executed in ICustomerRepository and the result to be passed into UserList via the constructor.
e.g.
public class UserList {
private String id;
private String email;
private String userType;
private String rolls;
private String partner;
private Long customersLinked; //better use Long instead of Integer
private String position;
private String status;
// constructor takes the number of linked customers as parameter
public UserList (Users user, Long customersLinked ) {
this.id = user.getId();
this.email = user.getEmail();
this.userType = user.getUserType();
this.rolls = user.getRolls();
this.partner = user.getPartner();
this.customersLinked = customersLinked;
this.position = user.getPosition();
this.status =user.getStatus();
}
//Getter and Setter
}
and then create the count query in ICustomerRepository
e.g.
public interface ICustomerRepository extends MongoRepository<Customer, String> {
//other methods
Long countByLinkedUsersIn(String id); //not so sure if this query works in mongo
}
and finally in your controller
Optional<Users> user = this.usersRepository.findById(userId);
Long count = this.usersRepository.countByLinkedUsersIn(userId);
userList.add(new UserList(user.get(), count));
P.S. I have a doubt for the query method: Long countByLinkedUsersIn(String id);. Usually when repository methods have "In" in their names, countByLinkedUsersIn, then it is expected as parameter a List and not a single user id. However if your previous method List<Customer> findAllByLinkedUsersIn(String id); worked for you, then this one should work too.

How to get a specific user by their national Id

I would like to add, edit and get users via an API I have created. I can add users and get all users added via API requests on Postman but I can't get data for a specific user when I request it on Postman as all I get is a null response with a 200 OK status. How can I be able to get a specific user's information using its national Id?
This is how my entity looks like;
public class users {
#Id
private String Id;
#Field("nationalId")
#JsonProperty("nationalId")
private String nationalId;
#Field("userName")
#JsonProperty("userName")
private String userName;
#Field("userEmail")
#JsonProperty("userEmail")
private String userEmail;
#Field("userPin")
#JsonProperty("userPin")
private String userPin;
#Field("userContact")
#JsonProperty("userContact")
private String userContact;
#Field("imageUrl")
#JsonProperty("imageUrl")
private String imageUrl;
#Field("loanLimit")
#JsonProperty("loanLimit")
private String loanLimit;
}
My controller class looks like this;
class Controller {
#Autowired
private user_service User_Service;
#PostMapping("/save_user")
private users save (#RequestBody users Users){return User_Service.save(Users);}
#PutMapping("/update_user")
private users update (#RequestBody users Users){return User_Service.update(Users);}
#GetMapping("/all")
private List<users> getAllusers(){return User_Service.getAllusers();}
#GetMapping("/user/{nationalId}")
private Optional <users> getusers(#PathVariable String nationalId) {return User_Service.getusers(nationalId);}
#DeleteMapping("/delete_user/{nationalId}")
private void deleteUser (#PathVariable String nationalId){User_Service.deleteUser(nationalId);}
}
My user service look like this;
public interface user_service {
users save (users Users);
users update (users Users);
List<users> getAllusers();
Optional<users> getusers(String nationalId);
void deleteUser (String nationalId);
}
You need to specify the name attribute in PathVariable annotation as #PathVariable(name="nationalId")
you need to have a method call like this to get a specific by national id.
Optional<user> getUserByNationalId(String nationalId);
for spring to understand what it is you want to fetch it cant figure it out by just passing in a string.
(And please rename your class User, and not users. The class represents a single user, and also please use camel casing)

Spring Data JPA Unable to locate Attribute with the given name

I was trying to use Spring Data JPA on Spring Boot and I kept getting error, I can't figure out what the problem is:
Unable to locate Attribute with the the given name [firstName] on
this ManagedType [com.example.h2demo.domain.Subscriber]
FirstName is declared in my entity class. I have used a service class with DAO before with different project and worked perfectly.
My Entity class (getters and setters are also in the class) :
#Entity
public class Subscriber {
#Id #GeneratedValue
private long id;
private String FirstName,LastName,Email;
public Subscriber(long id, String firstName, String lastName, String email) {
this.id = id;
this.FirstName = firstName;
this.LastName = lastName;
this.Email = email;
}
}
...
My Repository Class
#Component
public interface SubscriberRepository extends JpaRepository<Subscriber,Long> {
Subscriber findByFirstName(String FirstName);
Subscriber deleteAllByFirstName(String FirstName);
}
My Service Class
#Service
public class SubscriberService {
#Autowired
private SubscriberRepository subscriberRepository;
public Subscriber findByFirstName(String name){
return subscriberRepository.findByFirstName(name);
}
public Subscriber deleteAllByFirstName(String name){
return subscriberRepository.deleteAllByFirstName(name);
}
public void addSubscriber(Subscriber student) {
subscriberRepository.save(student);
}
}
And My Controller class:
#RestController
#RequestMapping("/subscribers")
public class SubscriberController {
#Autowired
private SubscriberService subscriberService;
#GetMapping(value = "/{name}")
public Subscriber findByFirstName(#PathVariable("name") String fname){
return subscriberService.findByFirstName(fname);
}
#PostMapping( value = "/add")
public String insertStudent(#RequestBody final Subscriber subscriber){
subscriberService.addSubscriber(subscriber);
return "Done";
}
}
Try changing private String FirstName,LastName,Email; to private String firstName,lastName,email;
It should work.
findByFirstName in SubscriberRepository tries to find a field firstName by convention which is not there.
Further reference on how properties inside the entities are traversed https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-property-expressions
The same problem was when i had deal with Spring Data Specifications (https://www.baeldung.com/rest-api-search-language-spring-data-specifications)
Initial piece of code was:
private Specification<Project> checkCriteriaByProjectNumberLike(projectNumber: String) {
(root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("project_number"), "%" + projectNumber)
}
The problem was in root.get("project_number"). Inside the method, I had to put the field name as in the model (projectNumber), but I sent the field name as in the database (project_number).
That is, the final correct decision was:
private Specification<Project> checkCriteriaByProjectNumberLike(projectNumber: String) {
(root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("projectNumber"), "%" + projectNumber)
}
After I change my entity class variables from capital letter to small letter for instance Username to username the method Users findByUsername(String username); is working for me now .
As per specification , the property names should start with small case.
...The resolution algorithm starts with interpreting the entire part (AddressZipCode) as the property and checks the domain class for a property with that name (uncapitalized)....
It will try to find a property with uncapitalized name. So use firstName instead of FristName and etc..

Repository doesn't save data to H2 in-memory db

Here is my controller method:
// CREATE A USER
#PostMapping("/register")
public String createUser(
#RequestBody User user
) {
if (userService.userExists(user)) {
return "User already exists";
}
userService.saveUser(user);
return "Good job!";
}
UserServiceBean
#Service
public class UserServiceBean {
private UserRepository userRepository;
#Autowired
public UserServiceBean(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User saveUser(User user) {
return userRepository.save(user);
}
public boolean userExists(User user) {
if (userRepository.findByUsername(user.getUsername()) == null) {
return false;
}
return true;
}
And my interface repository:
UserRepository
public interface UserRepository extends CrudRepository<User, Long> {
// TODO: 29.01.17 Create a query to find all todos for logged in user
#Query("select td from User u inner join u.toDoItems td where u = :user")
public Iterable<ToDoItem> findAllToDosForLoggedInUser(#Param("user") User user);
public User findByUsername(String username);
}
And here is my User Entity (getters and setters ommited)
#Entity
#Table (name = "USERS")
public class User extends BaseEntity {
#Column(name = "USERNAME")
private String username;
// TODO: 28.01.17 Find a way to store hashed and salted pws in DB
#Column(name = "PASSWORD")
private String password;
#Column(name = "EMAIL")
private String email;
// user can have many ToDoItems
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private Set<ToDoItem> toDoItems;
// JPA demands empty constructor
public User() {}
public User(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
}
When I shoot JSON at my localhost:8080/register:
{
"username":"ss",
"password":"mkyong.com",
"email":"asdasd#wp.pl"
}
I get response Good job! so it works fine. But when I check my DB at localhost:8080/console it just has Test Table and new User is not added.
I've got my hibernate ddl setup in application.properties set:
# Console to H2 database to check data
spring.h2.console.enabled=true
spring.h2.console.path=/console
spring.jpa.hibernate.ddl-auto=create-drop
So, how do I update my code that it creates table USERS and save created user into that db? I'm going to change my db later on, just using H2 to check if my controllers work fine but it shouldn't matter here.
EDIT:
Here is my RepositoryConfiguration.java:
#Configuration
#EnableAutoConfiguration
#EntityScan(basePackages = {"com.doublemc.domain"})
#EnableJpaRepositories(basePackages = {"com.doublemc.repositories"})
#EnableTransactionManagement
public class RepositoryConfiguration {
}
EDIT2:
When I want to register the same User again (using same JSON) then it gives me "User already exists" resposne so it is already in the db... Why can't I see it then? Maybe I've got H2 somewhere else? Not in the basic /console or different port? How can I check this?
I think you're missing the transactional part of your service. Did you define a transaction manager in your spring context ?
If so, you need to add the annotation #Transactional into your service. For example :
#Service
public class UserServiceBean {
#Transactional
public User saveUser(User user) {
return userRepository.save(user);
}
}
I had to add:
spring.datasource.url=jdbc:h2:~/test
spring.datasource.driver-class-name=org.h2.Driver
to application.properties and it works great now. I just thought I don't need it becasue Spring will auto-configure it for me but apparently it doesn't.

Categories

Resources