I have two classes one for Customers and one for Transaction. In the transaction, I have a field custID(int) which is also present in Customer. I have all the getter and setter, repos, services, and controller as well. But in one of my methods in the service layer, I am getting NoSuchElementException.
I understand that while the code runs and checks for a record in the database with passed custID, it cannot find the record. But I have mentioned what to do in such case. But my code doesn't move to the code block at all.
Customer Class
#Entity
#Table(name = "customers")
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int custID;
private String custName;
private String email;
private String phone;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy")
private LocalDate registrationDate;
Transaction Class
#Entity
#Table(name = "transactions")
public class Transaction {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int transID;
private int custID;
private int transAmount;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy")
private LocalDate transDate;
Service layer from where the issue arises:
TransactionServiceImpl
#Override
public Transaction addTransaction(CustomerTransaction customerTransaction) {
Customer customer = customerTransaction.getCustomer();
Transaction transaction = customerTransaction.getTransaction();
if(customerService.findCustomerByID(transaction.getCustID()) == null) {
customerService.addCustomer(customer);
return transactionRepository.save(transaction);
} else{
return transactionRepository.save(transaction);
}
}
What I am doing is, pass a wrapper object CustomerTransaction that has the info for the customer and transaction. And check if the customer is already registered with custID. If its there, it only records the transaction(which works fine as in 'else' block). But if it is not there I want to record the customer and the transaction both as in 'if' block. But it throws the NoSuchElementException: No value present.
But if I am to pass only the customer details via customer's service layer it adds the customer.
CustomerServiceImpl
#Override
public Customer addCustomer(Customer customer) {
return customerRepository.save(customer);
}
Postman Requests:
For customer only:
"custName": "Bibek Bhattarai",
"email": "spongebob#gmail.com",
"phone": 9803064423,
"registrationDate": "03-01-2023"
}
For customerTransaction:
{
"customer":{
"custName": "Sponge Bob",
"email": "spongebob#gmail.com",
"phone": 9803064423,
"registrationDate": "03-01-2023"
},
"transaction":{
"custID":9,
"transAmount": 5000,
"transDate": "04-01-2023"
}
}
You may get this exception because the value returned by findCustomerByID() is an Optional of Customer (Optional<Customer>) and not null.
Instead of customerService.findCustomerByID(transaction.getCustID()) == null you should have customerService.findCustomerByID(transaction.getCustID()).isEmpty().
Related
I have a springboot application with JPA. The ORM setup is a ManyToOne and I have roughly followed the excellent post here from Vlad so that I have only setup the #ManyToOne on the child.
My Entities are HealthCheck (Many) which must have a Patient (One). The issue is that when I retrieve a HealthCheck via my Rest controller instead of getting just the id of the patient I get the whole entity.
For my project I probably will get the whole patient with a HealthCheck, but I would like to know how i could get just the HealthCheck with the patient_id instead of the whole patient entity if I so needed to do so.
HEALTH CHECK
#AllArgsConstructor
#NoArgsConstructor
#Entity
#Data
public class HealthCheck {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private Patient patient;
//Getters and Setters
PATIENT
#Entity
#Data
public class Patient {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#Column(unique = true)
#NotEmpty(message = "Name must not be null or empty")
private String name;
// Getters and Setters
The HealthCheckServiceImpl uses the derived queries to get one by id, and its this call thats used to get a HealthCheck by the REST controller:
#Override
public HealthCheck get(Long id) {
log.info("Getting a single Health Check");
return healthCheckRepository.getById(id);
}
The result of a call to the REST controller results in something like:
{
"id": 2,
"patient": {
"id": 2,
"name": "Jono",
"hibernateLazyInitializer": {}
},
"other fields": "some comment",
"hibernateLazyInitializer": {}
}
Note1 the whole patient entity is returned
Note2 that because I have only used the ManyToOne annottaion on the child side I dont get the Jackson recursion issues some others do
QUESTION: How can I control the returned HealthCheck so that it includes the patient_id not the whole object?
UPDATE
The line that calls to the service is :
#GetMapping("get/{id}")
public ResponseEntity<HealthCheck> getHealthCheck(#PathVariable("id") Long id) {
return ResponseEntity.ok()
.header("Custom-Header", "foo")
.body(healthCheckService.get(id));
I breakpoint on healthCheckService.get(id)) but noting on the debugger looks like it contains an entity reference:
UPDATE2
Well, it seems you're returning your entities objects directly from your controller.
The issue is that when I retrieve a HealthCheck via my Rest controller instead of getting just the id of the patient I get the whole entity.
You can use the DTO Pattern explained here and here as response in your controller.
That, also decouples your domain from your controller layer.
This is a simplified example:
#Data
#NoArgsConstructor
public class HealthCheckDto {
private Long id;
private PatientDto patient;
private String otherField;
public HealthCheckDto(HealthCheck healthCheck) {
this.id = healthCheck.getId();
this.patient = new PatientDto(healthCheck.getPatient());
this.otherField = healthCheck.getOtherField();
}
}
#Data
#NoArgsConstructor
public class PatientDto {
private Long id;
public PatientDto(Patient patient) {
this.id = patient.getId();
}
}
// In your controller
public ResponseEntity<HealthCheckDto> getHealthCheck(Long id) {
HealthCheck healthCheck = healthCheckService.getById(id);
return ResponseEntity.ok(new HealthCheckDto(healthCheck));
}
When I pass data by POST request to my TodoItem model, only the columns specified by #column get filled in, but the #JoinColumn column is null even if I use the column name in the JSON I'm sending over. My GET request API work just fine however, so I omitted them from controller code.
TodoItem.java
#Entity
#Table(name = "todo_item")
public class TodoItem {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name = "todo")
private String todo;
#Column(name = "completed")
private boolean completed;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private User user;
public TodoItem() {
}
public TodoItem(String todo, boolean completed) {
this.todo = todo;
this.completed = completed;
}
// setters and getters
}
My constructor doesn't mention user_id, don't think it needs it though, but I may be wrong.
TodoController.java
#RestController
#RequestMapping("/api")
public class TodoController {
#Autowired
private UserRepository userRepository;
#Autowired
private TodoRepository todoRepository;
#PostMapping("/addItem")
public TodoItem addTodoItem(#RequestBody TodoItem todoItem) {
return this.todoRepository.save(todoItem);
}
}
I send POST to http://localhost:8080/api/addItem
Request:
{
"todo": "finish portfolio",
"completed": false,
"user_id": 1
}
However in my MySQL workbench, todo and completed get populated properly but user_id says null
In spring data (using hibernate or JPA), when saving entity which has reference to another entity . you must first fetch the referenced object first by id and then set it using setter in persistence entity. after that you can save and get (FK column) to be saved.
for example you first must use user repository and call
User user = userRepository.findById(userId);
and then
todoItem.setUser(user);
after that you can save item and FK column will get populated.
I think this the way to save reference entity. you can not relay on int id only.
also your request body JSON must be like this :
{
"todo": "finish portfolio",
"completed": false,
"user": {
"user_id":1
}
}
also it best practice to define and use DTO object instead of entity itself.
I know there are a lot of similar threads out there but i just can't figure it out from those threads on how to overcome this problem.
I have 3 classes Car, Brand, Color.
A Car has just one Brand and a list of Colors.
Brand has a List of Cars.
Color does not have any relation.
Getters, Setters, ToString and Constructors are not provided for simplicity sake.
I'm able to save objects into database and database is already populated.
--------------------------------------------------------------------------------
#Entity
#Table(catalog = "spring_project")
public class Car {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String model;
#ManyToMany(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
#JoinTable( name = "car_color", catalog = "spring_project",
joinColumns = { #JoinColumn(name = "car_id") },
inverseJoinColumns = { #JoinColumn(name = "colors_id") }
)
private List<Color> colors = new ArrayList<>();
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name="brand_id", referencedColumnName="id")
private Brand brand;
--------------------------------------------------------------------------------
#Entity
#Table(catalog = "spring_project")
public class Brand {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(mappedBy = "brand", fetch = FetchType.LAZY)
private List<Car> cars = new ArrayList<>();
--------------------------------------------------------------------------------
#Entity
#Table(catalog = "spring_project")
public class Color {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
--------------------------------------------------------------------------------
Everything runs just fine if i fetch like Eager, but i know it is a bad practice and it should be used Lazy loading instead. But i keep getting the LazyInitializationException.
I understand from the error that a session is required but i dont know how to provide one since im working with Spring Data JPA neither where i should declare one...
#SpringBootApplication
public class SrpingJpaApplication {
private static final Logger log =
LoggerFactory.getLogger(SrpingJpaApplication.class);
public static void main(String[] args) {
SpringApplication.run(SrpingJpaApplication.class, args);
}
#Bean
public CommandLineRunner demo(CarRepository carRepository,
ColorRepository colorRepository,
BrandRepository brandRepository) {
return (args) -> {
log.info("Reads all cars....");
for (Car c : carRepository.findAll()) {
System.out.println(c.toString());
}
};
}
}
Thank you so much.
Edited----->>>
The error is thrown on c.toString();
Error: Caused by: org.hibernate.LazyInitializationException: could not initialize
proxy [com.readiness.moita.SrpingJPA.Models.Brand#1] - no Session
The default for the #OneToMany annotation is FetchType.LAZY so your collections are loaded lazily.
In order to be able to access the collection after you've retrieved the object you need to be in a transactional context (you need an open session)
When you call:
carRepository.findAll();
internally a new session is created, the object is retrieved and as soon as the findAll method returns the session is closed.
What you should do is make sure you have an open session whenever you access the lazy collection in your Car object (which the toString does).
The simplest way is to have another service handle the car loading and annotate the showCars method with #Transactional the method is in another service because of the way AOP proxies are handled.
#Service
public CarService {
final CarRepository carRepository;
public CarService(CarRepository carRepository) {
this.carRepository = carRepository;
}
#Transactional
public void showCars(String... args) {
for (Car c : carRepository.findAll()) {
System.out.println(c.toString());
}
}
}
and then you call:
#Bean
public CommandLineRunner demo(CarService carService) {
return (args) -> service.showCars(args);
}
Because the FetchType of Brand is lazy, it will not automatically be loaded into the session with call to fetchAll(). To have it automatically load into the session, you need to:
Change
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name="brand_id", referencedColumnName="id")
private Brand brand;
to
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.EAGER)
Ex
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name="brand_id", referencedColumnName="id")
private Brand brand;
If you do not want to set the fetch type to eager, then you need to move your call to toString to a service method Ex
#Component
public CarService implements ICarService {
#Autowired
CarRepository carRepository;
#Transactional
public void printAllCars() {
for (Car c : carRepository.findAll()) {
System.out.println(c.toString());
}
}
}
The correct way to do this however would be to write a criteria query or hql
I understand by default that the CrudRepository.save method inserts and updates based on a primary key.
Consider the following entity
#Entity
public class BookEntity {
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#Basic
#Column(name = "isbn", unique = true)
private String isbn;
#Basic
#Column(name = "title")
private String title;
#Basic
#Column(name = "author")
private String author;
#Basic
#Column(name = "publication_date")
private Date publicationDate;
#Basic
#Column(name = "rank")
private Integer rank;
}
Because I can not have books with the same isbn in the table and I don't want to worry about generating ids I normally post the following json to an enpoint
{
"isbn": "10932011",
"title": "harry",
"author": "jk",
"publicationDate": "2018-10-10",
"rank": 1000
}
Which returns the following with an auto generated id
{
"id": 3,
"isbn": "10932011",
"title": "harry",
"author": "jk",
"publicationDate": "2018-10-10T00:00:00.000+0000",
"rank": 1000
}
If I make second call using the same isbn I'll get a constraint error
{
"isbn": "10932011",
"title": "harry",
"author": "jk3",
"publicationDate": "2018-10-10",
"rank": 1000
}
But I would like to in fact update the book with the same isbn and not have to specify the auto generated id in the post json as this is not important to me. Is there a way of doing this without having to add logic to a service class?
You can get the BookEntity, change it and save it.
Since your isbn is unique, you can do something like this:
BookEntity book = bookRepository.findByIsbn(isbn);
book.setWhateverFieldYouWant(value);
bookRepository.save(book);.
Or another solution
You can create a method with #Query annotation in BookRepository:
#Query("UPDATE BookEntity b SET b.whateverField = :value WHERE b.isbn = :isbn")
void updateBook(#Param("value") String value, #Param("isbn") String isbn);
and call it from service:
bookRepository.updateBook(value, isbn);
Since you are using Hibernate, you can also take a look at the NaturalId API Hibernate provides. In addition to your generated ID you annotate your isbn as a #NaturalId and then use the NaturalId API to retrieve your books.
#Entity
public class BookEntity {
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#NaturalId
#Column(name = "isbn")
private String isbn;
Load example:
BookEntity book = entityManager.unwrap(Session.class)
.bySimpleNaturalId(BookEntity.class)
.load("your isbn goes here");
For further reading on NaturalIds take a look at
this article (its sample is with isbns) or this one.
A big plus of NaturalIds is you can benefit of hibernate caching mechanisms.
The best way I have found is based in this article using #NaturalId:
Step 1. Create a NaturalRepository interface (to fine-tune the built-in JpaRepository):
#NoRepositoryBean
interface NaturalRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
Optional<T> findBySimpleNaturalId(ID naturalId);
}
Step 2. Extend SimpleJpaRepository and implement NaturalRepository(to customize it):
import org.hibernate.Session;
#Transactional(readOnly = true)
class NaturalRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements NaturalRepository<T, ID> {
final EntityManager entityManager;
NaturalRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
#Override
Optional<T> findBySimpleNaturalId(ID naturalId) {
return entityManager.unwrap(Session.class)
.bySimpleNaturalId(this.getDomainClass())
.loadOptional(naturalId);
}
}
Step 3. Tell Spring to use this customized repository base:
#SpringBootApplication
#EnableJpaRepositories(repositoryBaseClass = NaturalRepositoryImpl.class)
class MySpringApp {
...
Step 4. Time to use #NaturalId:
#Entity
public class BookEntity {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#NaturalId
#Column(name = "isbn")
private String isbn;
Step 5. Define a classical Spring repository:
#Repository
interface BookRepository extends NaturalRepository<Book, String> {
// You can add other methods here like:
List<Book> findByTitle(String title);
List<Book> findByAuthor(String author);
}
Take note:
its now using #Repository (instead of #RepositoryRestResource)
that it should now extends NaturalRepository (instead of CrudRepository)
the second type should be String
Step 6. Finally:
...
#Autowired
BookRepository repository;
...
String isbn = "...";
try {
Book book = repository.findBySimpleNaturalId(isbn).get();
book.author = "gal";
repository.save(book);
} catch(NoSuchElementException notfound) {
...
}
The nice thing is that you can reuse the NaturalRepository and NaturalRepositoryImpl in any other project or with any other repository for other tables.
I have defined customer entity
#Entity
#Table(name = "customer")
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
and CrudRepository
public interface CustomerRepo extends CrudRepository<Customer, Long> {
}
if I use CustomerRepo.findById method for finding Customer
#Autowired
CustomerRepo repo;
Optional<Customer> dbCustomer = repo.findById(id);
how can I get name of that customer. I cannot use getter then.
so I'm interested is there any solution of using getters of Optional, or I need to use other method for finding Customer by id?
Optional<Customer> is returned, because it is not guaranteed that there will be such a customer with the requested ID in the database.
Instead of returning null it simply means that Optional.isPresent() will return false when the ID does not exist.
According to the API Docs (https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html#findById-ID-):
Returns:
the entity with the given id or Optional#empty() if none found
You will therefore probably want to simply use the methods on Optional to check whether it contains a Customer (i.e. a Customer with that ID exists), and then get the name like so:
Optional<Customer> dbCustomer = repo.findById(id);
if(dbCustomer.isPresent()) {
Customer existingCustomer = dbCustomer.get();
String nameWeWanted = existingCustomer.getName();
//operate on existingCustomer
} else {
//there is no Customer in the repo with 'id'
}
Alternatively you can try callback style (shown with Java 8 Lambda):
Optional<Customer> dbCustomer = repo.findById(id);
dbCustomer.ifPresent(existingCustomer -> {
String nameWeWanted = existingCustomer.getName();
//operate on existingCustomer
});
It is worth noting that it is possible to check existence of the ID without actually retrieving/loading the entity by using the interface method:
boolean CrudRepository.existsById(ID id)
This saves an entity load, but it still requires a database roundtrip.
Try to use another method for finding Customer:
#Autowired
CustomerRepo repo;
Customer dbCustomer = repo.findOne(id);