Issue
I'm writing test code for a simple service layer using jpa data entity, but entity's relationship doesn't seem to work properly.
I googled but couldn't find an answer. I'm not sure what's wrong.
Please check the code below
Codes
(0) test property
spring.datasource.url=jdbc:h2:mem:testdb;DATABASE_TO_UPPER=false;MODE=MySQL
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.jpa.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
(1) entities
#Entity
#Getter
#Setter
#Table(name = "users")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public long id;
private String username;
private String password;
#OneToMany(mappedBy = "user")
private List<Authority> authList;
public Set<String> getAuthName() {
if (this.authList == null || this.authList.isEmpty()) return null;
Set<String> authNameSet = new HashSet<>();
authList.forEach(e -> authNameSet.add(e.getStringName()));
return authNameSet;
}
}
//----------
#Entity
#Getter
#Setter
#Table(name = "authorities")
public class Authority implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public long id;
#ManyToOne
#JoinColumn(name = "user_id")
private User user;
#Enumerated(EnumType.STRING)
#Column(length = 10)
private AuthorityName name;
}
(2) repositories
#Repository
public interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
}
//----------
#Repository
public interface AuthorityRepository extends CrudRepository<Authority, Long> {
List<Authority> findByUser(User user);
}
(3) service layer to test
#Service
public class AuthorityService {
// this is the method i want to test
public boolean isUserHaveAuthorityOf(User user, AuthorityName authOf) {
if (user == null || authOf == null) throw new InternalError("msg");
if (user.getAuthName() == null) return false;
return user.getAuthName().contains(authOf.name());
}
}
(4) test code
import static org.junit.jupiter.api.Assertions.*;
#TestPropertySource("/application-test.properties")
#SpringBootTest
#Transactional
class AuthorityServiceTest {
#Autowired private UserRepository userRepository;
#Autowired private AuthorityRepository authorityRepository;
#Autowired private AuthorityService authorityService;
private User sampleUser;
#BeforeEach
void setupDB() {
User user = new User();
user.setId(1);
user.setUsername("name");
user.setPassword("passwd");
user.setEmail("email#dot.com");
user.setBirth("20000101");
user.setAddress("addr");
user.setAddressDetail("addrDetail");
user.setZipCode("12345");
user.setDeleted(false);
user.setDisabled(false);
user.setBlock(false);
userRepository.save(user);
sampleUser = user;
}
#Test
void testIsUserHaveAuthorityOfNormalCase() {
assertFalse(authorityService.isUserHaveAuthorityOf(sampleUser, AuthorityName.USER));
assertFalse(authorityService.isUserHaveAuthorityOf(sampleUser, AuthorityName.ADMIN));
Authority auth = new Authority();
auth.setUser(sampleUser);
auth.setName(AuthorityName.USER);
authorityRepository.save(auth);
assertNotNull(sampleUser.getAuthName()); // it is null!
// I thought it was because of a caching, so I get a new user from the db.
User flushedUser = userRepository.findById(sampleUser.getId()).get();
assertNotNull(flushedUser.getAuthName()); // but still null
}
#AfterEach
void cleanDB() {
authorityRepository.deleteAll();
userRepository.deleteAll();
}
}
Thank you
You have a bidirectional relationship between User and Authority so you should keep the relationship in sync. As well as setting the User in an Authority you should also add the Authority to the the User
public void addAuthority(Authority authority) {
if (authList == null) {
authList = new ArrayList<>();
}
authList.add(authority);
authority.setUser(this);
}
It would also be helpful to see how you are persisting the sampleUser
Related
I want to test Transactional operation in my project. Basically, I want to roll back the userService.saveUser() operation, if an exception is thrown. I have simplified the classes, and you can find it below.
A user must live in an address. An address can have multiple users.
Address Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
#Entity
public class Address {
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "STREET")
#NotNull
private String street;
#ToString.Exclude
#OneToMany(mappedBy = "address")
private Set<User> users = new HashSet<>();
}
User Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "USER")
public class User {
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "FIRSTNAME", nullable = false)
#NotNull
private String firstName;
#ManyToOne(fetch = FetchType.LAZY)
private Address address;
}
Repositories
public interface AddressRepository extends CrudRepository<Address, Long> {
}
public interface UserRepository extends CrudRepository<User, Long> {
}
UserService Class
#Service
#Slf4j
#AllArgsConstructor
public class UserService {
#Autowired
AddressRepository addressRepository;
#Autowired
UserRepository userRepository;
#Transactional
public void saveUser(String firstName, String street) {
var address1 = Address.builder.street(street).build();
// to make sure that I have "id" of the address when I am saving it.
var addressSaved = addressRepository.save(address1);
if ("f1".equals(firstName))
throw new RuntimeException("some exception");
var user = User.builder()
.firstName(firstName)
.address(addressSaved)
.build();
// this operation can also throw DataIntegrityViolationException
userRepository.save(user);
}
}
This is my test class
#SpringBootTest
class UserServiceIT {
#Autowired
AddressRepository addressRepository;
#Autowired
UserRepository userRepository;
#Autowired
UserService userService;
#BeforeEach
void beforeEach() {
userRepository.deleteAll();
addressRepository.deleteAll();
}
#Test
void test_saveUser() {
assertThrows(RuntimeException.class,() -> userService.saveUser("f1", "s1"));
assertEquals(0, userRepository.count());
assertEquals(0, addressRepository.count());
}
#Test
void test_saveUser2() {
// column: nullable = false will throw the exception
assertThrows(DataIntegrityViolationException.class,() -> userService.saveUser(null, "s1"));
assertEquals(0, userRepository.count());
assertEquals(0, addressRepository.count());
}
}
Both of the tests give assertion error on address count (Address is saved and user is not saved). I expect address to be roll backed (and not to be saved) since there is an error after saving the address, and while saving the user (some condition is violated, therefore 2 saves must be roll backed). What am I doing wrong?
application.yml for test environment
spring:
devtools:
restart:
enabled: false
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=false
driverClassName: org.h2.Driver
username: sa
password: 123
h2:
console:
enabled: false
jpa:
database-platform: org.hibernate.dialect.H2Dialect
database: H2
show-sql: false
hibernate:
ddl-auto: create
You can reach the whole sample project from this link: https://wetransfer.com/downloads/7cb870266e2e20f610b44d3cc9f229c220220308071438/7b88a2700076a3e53771e389c796cfe420220308071438/c777ab
The code you posted here differs from what is actually exists in the original code you uploaded.
original code:
#Transactional
void saveUser(String firstName, String street) {
var address = Address.builder().street(street).build();
var addressSaved = addressRepository.save(address);
if ("f1".equals(firstName))
throw new RuntimeException("f1");
var user = Person.builder()
.firstName(firstName)
.address(addressSaved)
.build();
personRepository.save(user);
}
This method actually have default access modifier so CGLIB is not able to override it and creates the needed logic. change access modifier of this method to public
I want to make login form with data from my own database. I made Entity User class:
#Entity
#Table(name = "Auth_data")
public class User {
#Id
#GeneratedValue
#Getter #Setter
private Long id;
#Column(name= "username")
#Getter #Setter
private String username;
#Column(name="password")
#Getter #Setter
private String password;
#Enumerated(EnumType.ORDINAL)
#Column(name="role")
#Getter #Setter
private Role role;
#Column(name = "client_ID")
#Getter #Setter
private Long clientID;
#Column(name = "instructor_ID")
#Getter #Setter
private Long instructorID;
public User() {}
public User(String username, String password, Role role, Long clientID, Long instructorID) {
this.username = username;
this.password = password;
this.role = role;
this.clientID = clientID;
this.instructorID = instructorID;
}
}
Also I made UserDetailsServiceIml class which implements UserDetailsService:
#Service
public class UserDetailsServiceIml implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
final User user = userRepository.findByUsername(username);
if(user == null) {
throw new UsernameNotFoundException(username);
}
Set<GrantedAuthority> grantedAuthorities = new HashSet< >();
grantedAuthorities.add(new SimpleGrantedAuthority(user.getRole().name()));
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
grantedAuthorities);
}
}
My User repository class:
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
I also tried make Query like this:
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
#Query("Select u from User u WHERE u.username=:username")
User findByUsername(#Param("username") String username);
}
But when I enter login and password into login form I get error, because User is Null after searching in database. How can I fix it?
UPD: SOLVED!
I don't know why and how. I found that hibernate created new table "auth_data", but not used my table "Auth_data". So, I renamed "Auth_data" to "auth_data" and now it's work! I also changed #Getter #Setter on #Data,changed in User repository on " User findByUsername(String username);" and changed type of field role from enum to String.
It is possible that you don't seed the database with the user you are trying to login. You can use an Application Runner class to seed it on every start if the user doesn't exist in the database. Also, you can apply the #Data annotation from Lombok on class level to generate getters and setters for all the fields.
package com.javahowtos.dataseeddemo.dataseed;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import com.javahowtos.dataseeddemo.model.User;
import com.javahowtos.dataseeddemo.repository.UserRepository;
#Component
public class UserDataLoader implements CommandLineRunner {
#Autowired
UserRepository userRepository;
#Override
public void run(String... args) throws Exception {
loadUserData();
}
private void loadUserData() {
if (userRepository.count() == 0) {
User user1 = new User("John", "Doe");
User user2 = new User("John", "Cook");
userRepository.save(user1);
userRepository.save(user2);
}
System.out.println(userRepository.count());
}
}
Source: https://javahowtos.com/guides/107-spring/376-how-to-seed-the-database-in-spring-boot-project.html
You have to be sure that you have a user with the appropriate username in the database (case sensitive), if you do not have a user you can insert some test users (with the migration tool, with custom component, or manually)
You probably need to add a unique index for username
If you extend JpaRepository and add method findByUsername spring will generate implementation and you do not need #Query, if you have multiple items you can use findFirstByUsername
I has a issue on my learn project, on case "if condition value is null and then else if condition value field is null" for example my code following these code :
For Entity Users.java :
#Entity
public class Users {
private Long id;
private String employeeId;
private String fullName;
private String username;
private String password;
...
public Users() {
}
Some Code Setter and Getter....
}
For Entity Employee.java :
#Entity
public Class Employee {
private Long id;
private String employeeId;
private String fullName;
...
public Employee() {
}
Some Code Setter and Getter....
}
and then for my Class Service i have case for insert data Employee with Repository. On case we have validation data before insert data to table Employee, we need to check table users not null and then on field employeeId should null. with my code following this :
For Repository UserRepo.java and EmployeeRepo.java :
#Repository
public interface EmployeeRepo extends CrudRepository<Employee, Long> {
}
#Repository
public interdace UsersRepo extends CrudRepository<Users, Long> {
#Transactional
#Modifying(clearAutomatically = true, flushAutomatically = true)
#Query("UPDATE Users u SET u.employeeId = :employeeId WHERE u.id = :id")
public void updateEmployeeIdUsers(#Param("id") Long id, #Param("employeeId") String employeeId);
}
For Service UsersService.java :
#Service("usersService")
public class UsersService {
#Autowired
private UsersRepo repo;
public Optional<Users> findById(Long id) {
return repo.findById(id);
}
public void updateEmployeeIdUsers(Long id, String employeeId) {
repo.updateEmployeeIdUsers(id, employeeId);
}
}
For Service EmployeeService.java :
#Service("employeeService")
public class EmployeeService {
#Autowired
private EmployeeRepo employeeRepo;
#Autowired
private UsersService userService;
public Employee insertEmployee(Employee employee) throws Exception {
Optional<Users> users = userService.findById(employee.getId());
Users userOptional = new Users(); **//on this my problem**
userOptional.getEmployeeId(); **//on this my problem**
if (!users.isPresent()) {
throw new Exception("User ID : "+ employee.getId() +" Not Founded");
}else if (!(userOptional == null)) { **//on this my problem**
throw new Exception("User employeID : "+ employee.getEmployeeId() +" Already Exist on Users");
}
String str1 = "TEST";
Long idUser = employee.getId();
userService.updateEmployeeIdUsers(idUser, str1);
return employeeRepo.save(employee);
}
}
on this code we have problem on else if userOptional is always NULL and i try to debug to see value on employeeId just i see always Null. so any idea with my problem because i try some case alway fail with my issue. please if any idea for my issue, can reply these my question. is very thank you of all about thing to answer my question.
For the proposed solution, I will assume the following:
There is relation between Employee and Users.
An Employee can be related with only one Users
username is the natural key of Users
employeeId is the natural key of Employee
So the entities:
#Entity
public class Users {
#Id
// This one is an example, you can use the configuration you need
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator= "users_seq")
#SequenceGenerator(name="users_seq", initialValue=1, allocationSize=1, sequenceName = "users_id_seq")
private Long id;
#Column(name = "fullname")
private String fullName;
// Probably this column should be unique and you need to configure in that way here and in your database
#Column
private String username;
#Column
private String password;
// Getter & setter & constructors
}
#Entity
public class Employee {
#Id
// This one is an example, you can use the configuration you need
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator= "employee_seq")
#SequenceGenerator(name="employee_seq", initialValue=1, allocationSize=1, sequenceName = "employee_id_seq")
private Long id;
/**
* Assuming this is your specific identifier for an employee (not related with database PK)
* If the assumption is correct, this column should be unique and you need to configure in
* that way here and in your database
*/
#Column(name = "employeeid")
private String employeeId;
/**
* Not sure if this relation could be nullable or not
*/
#OneToOne
#JoinColumn(name = "users_id")
private Users users;
// Getter & setter & constructors
}
As you can see, there are no "repeated columns" in both entities and there is an unidirectional OneToOne relation between Employee and Users. If you need a bidirectional one, this link will help you with it: Bidirectional OneToOne
The repositories:
#Repository
public interface UsersRepository extends CrudRepository<Users, Long> {
Optional<Users> findByUsername(String username);
}
#Repository
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
Optional<Employee> findByEmployeeId(String employeeId);
}
The services:
#Service
public class UsersService {
#Autowired
private UsersRepository repository;
public Optional<Users> findByUsername(String username) {
return Optional.ofNullable(username)
.flatMap(repository::findByUsername);
}
public Optional<Users> save(Users user) {
return Optional.ofNullable(user)
.map(repository::save);
}
}
#Service
public class EmployeeService {
#Autowired
private EmployeeRepository repository;
#Autowired
private UsersService usersService;
public Optional<Employee> insert(Employee newEmployee) {
/**
* The next line don't make sense:
*
* Optional<Users> users = userService.findById(employee.getId());
*
* I mean:
*
* 1. Usually, id column is configured with #GeneratedValue and manage by database. So you don't need to ask
* if that value exists or not in Users.
*
* 2. Even if you are including id's values manually in both entities what should be "asked" is:
*
* 2.1 Is there any Users in database with the same username than newEmployee.users.username
* 2.2 Is there any Employee in database with the same employeeId
*
* Both ones, are the natural keys of your entities (and tables in database).
*/
return Optional.ofNullable(newEmployee)
.filter(newEmp -> null != newEmp.getUsers())
.map(newEmp -> {
isNewEmployeeValid(newEmp);
// Required because newEmp.getUsers() is a new entity (taking into account the OneToOne relation)
usersService.save(newEmp.getUsers());
repository.save(newEmp);
return newEmp;
});
}
private void isNewEmployeeValid(Employee newEmployee) {
if (usersService.findByUsername(newEmployee.getUsers().getUsername()).isPresent()) {
throw new RuntimeException("Username: "+ newEmployee.getUsers().getUsername() +" exists in database");
}
if (repository.findByEmployeeId(newEmployee.getEmployeeId()).isPresent()) {
throw new RuntimeException("EmployeeId: "+ newEmployee.getEmployeeId() +" exists in database");
}
}
}
After read comments I already understand your problem.
Users users = userService.findById(employee.getId()).orElseThrow(() -> new Exception("User ID : "+ employee.getId() +" Not Founded"));
And now you can get your employeeId from users from returned userService.findById(employee.getId());
Example:
String employeeId = users.getEmployeeId(); // reference to your code
But in this case in my opinion you should make relation #OneToOne between users and employee or extend users in employee class.
One-To-One relation in JPA,
hibernate-inheritance
Given a User entity with the following attributes mapped:
#Entity
#Table(name = "user")
public class User {
//...
#Id
#GeneratedValue
#Column(name = "user_id")
private Long id;
#Column(name = "user_email")
private String email;
#Column(name = "user_password")
private String password;
#Column(name = "user_type")
#Enumerated(EnumType.STRING)
private UserType type;
#Column(name = "user_registered_date")
private Timestamp registeredDate;
#Column(name = "user_dob")
#Temporal(TemporalType.DATE)
private Date dateOfBirth;
//...getters and setters
}
I have created a controller method that returns a user by ID.
#RestController
public class UserController {
//...
#RequestMapping(
value = "/api/users/{id}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> getUser(#PathVariable("id") Long id) {
User user = userService.findOne(id);
if (user != null) {
return new ResponseEntity<User>(user, HttpStatus.OK);
}
return new ResponseEntity<User>(HttpStatus.NOT_FOUND);
}
//...
}
A service in my business logic layer.
public class UserServiceBean implements UserService {
//...
public User findOne(Long id) {
User user = userRepository.findOne(id);
return user;
}
//...
}
And a repository in my data layer.
public interface UserRepository extends JpaRepository<User, Long> {
}
This works fine, it returns everything about the user, but I use this in several different parts of my application, and have cases when I only want specific fields of the user.
I am learning spring-boot to create web services, and was wondering: Given the current implementation, is there a way of picking the attributes I want to publish in a web service?
If not, what should I change in my implementation to be able to do this?
Thanks.
Firstly, I agree on using DTOs, but if it just a dummy PoC, you can use #JsonIgnore (jackson annotation) in User attributes to avoid serializing them, for example:
#Entity
#Table(name = "user")
public class User {
//...
#Column(name = "user_password")
#JsonIgnore
private String password;
But you can see there, since you are not using DTOs, you would be mixing JPA and Jackson annotations (awful!)
More info about jackson: https://github.com/FasterXML/jackson-annotations
I have two classes that have a one-to-many relation. When I try to access the lazily loaded collection I get the LazyInitializationException.
I have been searching the web for a while and now I know that I get the exception because the session that was used to load the class which holds the collection is closed.
However, I did not find a solution (or at least I did not understand them). Basically I have these classes:
User
#Entity
#Table(name = "user")
public class User {
#Id
#GeneratedValue
#Column(name = "id")
private long id;
#OneToMany(mappedBy = "creator")
private Set<Job> createdJobs = new HashSet<>();
public long getId() {
return id;
}
public void setId(final long id) {
this.id = id;
}
public Set<Job> getCreatedJobs() {
return createdJobs;
}
public void setCreatedJobs(final Set<Job> createdJobs) {
this.createdJobs = createdJobs;
}
}
UserRepository
public interface UserRepository extends JpaRepository<User, Long> {}
UserService
#Service
#Transactional
public class UserService {
#Autowired
private UserRepository repository;
boolean usersAvailable = false;
public void addSomeUsers() {
for (int i = 1; i < 101; i++) {
final User user = new User();
repository.save(user);
}
usersAvailable = true;
}
public User getRandomUser() {
final Random rand = new Random();
if (!usersAvailable) {
addSomeUsers();
}
return repository.findOne(rand.nextInt(100) + 1L);
}
public List<User> getAllUsers() {
return repository.findAll();
}
}
Job
#Entity
#Table(name = "job")
#Inheritance
#DiscriminatorColumn(name = "job_type", discriminatorType = DiscriminatorType.STRING)
public abstract class Job {
#Id
#GeneratedValue
#Column(name = "id")
private long id;
#ManyToOne
#JoinColumn(name = "user_id", nullable = false)
private User creator;
public long getId() {
return id;
}
public void setId(final long id) {
this.id = id;
}
public User getCreator() {
return creator;
}
public void setCreator(final User creator) {
this.creator = creator;
}
}
JobRepository
public interface JobRepository extends JpaRepository<Job, Long> {}
JobService
#Service
#Transactional
public class JobService {
#Autowired
private JobRepository repository;
public void addJob(final Job job) {
repository.save(job);
}
public List<Job> getJobs() {
return repository.findAll();
}
public void addJobsForUsers(final List<User> users) {
final Random rand = new Random();
for (final User user : users) {
for (int i = 0; i < 20; i++) {
switch (rand.nextInt(2)) {
case 0:
addJob(new HelloWorldJob(user));
break;
default:
addJob(new GoodbyeWorldJob(user));
break;
}
}
}
}
}
App
#Configuration
#EnableAutoConfiguration
#ComponentScan
public class App {
public static void main(final String[] args) {
final ConfigurableApplicationContext context = SpringApplication.run(App.class);
final UserService userService = context.getBean(UserService.class);
final JobService jobService = context.getBean(JobService.class);
userService.addSomeUsers(); // Generates some users and stores them in the db
jobService.addJobsForUsers(userService.getAllUsers()); // Generates some jobs for the users
final User random = userService.getRandomUser(); // Picks a random user
System.out.println(random.getCreatedJobs());
}
}
I have often read that the session has to be bound to the current thread, but I don't know how to do this with Spring's annotation based configurations.
Can someone point me out how to do that?
P.S. I want to use lazy loading, thus eager loading is no option.
Basically, you need to fetch the lazy data while you are inside of a transaction. If your service classes are #Transactional, then everything should be ok while you are in them. Once you get out of the service class, if you try to get the lazy collection, you will get that exception, which is in your main() method, line System.out.println(random.getCreatedJobs());.
Now, it comes down to what your service methods need to return. If userService.getRandomUser() is expected to return a user with jobs initialized so you can manipulate them, then it's that method's responsibility to fetch it. The simplest way to do it with Hibernate is by calling Hibernate.initialize(user.getCreatedJobs()).
Consider using JPA 2.1, with Entity graphs:
Lazy loading was often an issue with JPA 2.0. You had to define at the entity FetchType.LAZY or FetchType.EAGER and make sure the relation gets initialized within the transaction.
This could be done by:
using a specific query that reads the entity
or by accessing the relation within business code (additional query for each relation).
Both approaches are far from perfect, JPA 2.1 entity graphs are a better solution for it:
http://www.thoughts-on-java.org/jpa-21-entity-graph-part-1-named-entity/
http://www.thoughts-on-java.org/jpa-21-entity-graph-part-2-define/
You have 2 options.
Option 1 : As mentioned by BetaRide, use the EAGER fetching strategy
Option 2 : After getting the user from database using hibernate, add the below line in of code to load the collection elements:
Hibernate.initialize(user.getCreatedJobs())
This tells hibernate to initialize the collection elements
Change
#OneToMany(mappedBy = "creator")
private Set<Job> createdJobs = new HashSet<>();
to
#OneToMany(fetch = FetchType.EAGER, mappedBy = "creator")
private Set<Job> createdJobs = new HashSet<>();
Or use Hibernate.initialize inside your service, which has the same effect.
For those who have not the possibility to use JPA 2.1 but want to keep the possibility to return a entity in their controller (and not a String/JsonNode/byte[]/void with write in response):
there is still the possibility to build a DTO in the transaction, that will be returned by the controller.
#RestController
#RequestMapping(value = FooController.API, produces = MediaType.APPLICATION_JSON_VALUE)
class FooController{
static final String API = "/api/foo";
private final FooService fooService;
#Autowired
FooController(FooService fooService) {
this.fooService= fooService;
}
#RequestMapping(method = GET)
#Transactional(readOnly = true)
public FooResponseDto getFoo() {
Foo foo = fooService.get();
return new FooResponseDto(foo);
}
}
You should enable Spring transaction manager by adding #EnableTransactionManagement annotation to your context configuration class.
Since both services have #Transactional annotation and default value property of it is TxType.Required, current transaction will be shared among the services, provided that transaction manager is on. Thus a session should be available, and you won't be getting LazyInitializationException.