I have a stateless REST backend. So no HTML views. Just JSON and REST endpoints.
Authentication is done with Json Web Tokens. The client sends a JWT in each request.
My backend takes the user's email from the subject claim in this JWT. Then it loads the UserModel from the database in class LiquidoUserDetailsService implements UserDetailsService { ...}
Each user is part of a team. But the Team is a big entity with a lot of information in it. So teams are only loaded lazily, when necessary:
UserModel.java
#Entity
#Table(name = "users")
public class UserModel extends BaseModel {
#NotNull
#NonNull
#Column(unique = true)
public String email;
#ManyToOne(fetch = FetchType.LAZY) // only load team info (with all info) if required
public TeamModel team;
[...]
}
Now I have a service that should return the team of the current user:
TeamService.java
#PreAuthorize(HAS_ROLE_USER)
#RequestMapping("/getOwnTeam")
#Transactional // [1]
public TeamModel getOwnTeam() {
// Get currently logged in user (that was loaded with info from JWT)
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LiquidoAuthUser authUser = (LiquidoAuthUser)authentication.getPrincipal();
// LiquidoAuthUser is the Adapter betwen spring's User and my USerModel
UserModel currentUser = authUser.getLiquidoUserModel()
TeamModel team = currentUser.getTeam() // <=== [2] throws LazyInitializationException
return team
}
Now I think I know where the problem is. But I do not yet have a clean solution for it.
My UserModel is loaded in class LiquidoUserDetailsService implements UserDetailsService But this happens very early, in a filter, when the HTTP request is processed. As it seams the #Transaction in my TeamService class is not yet started at that time.
Then when the code enters the getOwnTeam() method, a new transaction is started [1]. But in there I cannot lazy load the user's team anymore. [2]
How can I model my users and teams, so that
The team data is only loaded when necessary
I can load the data manually when neccessary
If you need different load startegy you can use:
Native sql when query
jpql with construction like join fetch
Entity Graph (https://www.baeldung.com/jpa-entity-graph)
The main benefit when you use such way to load is single request to database.
You can read more https://thorben-janssen.com/lazyinitializationexception/
Your object in deatached state - this is reason of LazyInitializationException (you cat move it to other state to load your object)
for example
entityManager.merge(deatachedEntity);
Related
I have 3 class that using Many to Many relationship in Entity level (herbibate)
They are Teams,Users and TeamUsers.
Since I am using hibernate they are all Fetch Lazy
Team:
Users:
TeamUsers:
and we have 3 interface (using by #Autowired in Service or repo layer) which is:
1.TeamRepository extends PagingAndSortingRepository<Team, Integer>
2.UserRepository extends PagingAndSortingRepository<User, Integer>
3.TeamUserRepository extends PagingAndSortingRepository<TeamUsers, Integer>
And here is the core code for many to many relationship mapping:
Team class:
#OneToMany(mappedBy = "team", fetch = FetchType.EAGER)
private Set<TeamUsers> team_users = new HashSet<TeamUsers>();
User class:
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<TeamUsers> team_users = new HashSet<TeamUsers>();
TeamUsers class:
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "TEAM_ID")
private Team team;
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "USER_ID")
private User user;
Now, my questions is, what is the best practice to play around this data?
for example:
1. I need all team information and team member from my DB
In restful controller get mapping function, what should I return? should I use TeamUser repository (or maybe one more service layer) to return findAll()? if do so the data will come as object collection of User and Team right(maybe also contain extra column in that table)? Can Json successfully contain that data?
2. I am on webpage and I want to delete a user from a team.
At this timepoint I know the TeamID and UserID from HTML, when I send the Post request to API, should I get User object by UserID, then get Team object by TeamID, then get(Query) TeamUsers object by giving User object and Team Object? or maybe just query by id(from TeamUsers on html) send to API and simply remove this TeamUsers entity?
I'm new to spring and frontend development and I am much apprecaite for your help!
I would suggest the following
If you want to return all the teams and for each team all its users, you should have a TeamService which call findAll method of Team repository this way you will get a list of Teams and for each team a set with its users. And if instead of that you want all of your users and for each one what are its teams you should do the other way, call the findAll method of User repository
Both solutions will serialize to json without problem as long as the objects have its constructor and getter and setter methods
If you want to delete only the relation between user and team you could have a method in your TeamEntity Spring Data repository like this
long deleteByTeamIdAndUserId(long teamId, long userId);
And spring will create a method to perform this action or you can write the query you like just above the method name to be more specific
#Modifying
#Query("delete from TeamUsers t where t.userId=:userId and t.teamId = :teamId")
long deleteByTeamIdAndUserId(long teamId, long userId);
So I'm trying to remove the the child in a onetomany relationship but I'm not sure if this is the right way to do. I was reading up how to do it online but many talked about entitymanager, cacasded, using queries etc. I'm unsure on which way to do it, usually I use crudrepository and simply do .save and .deleteById etc.
Here's what I have so far
#Entity
public class User
#OneToMany(orphanRemoval = true, cascade = CascadeType.PERSIST)
private List<Payment>payment = new ArrayList<Payment>();
getters/setters
#Service
public class UserService {
public void addPayment(User user, Payment payment) {
user.getPayment().add(payment);
}
public void removePayment(User user, Payment payment) {
user.getPayment().remove(payment);
}
Do I have to mess with the cascade type or entitymanager here?
Assuming the user method parameter is an managed Entity already associated with the session, then yes, that's the correct way to do it.
When the session gets flushed, Hibernate will delete the Payment instance which was removed from the users payments.
You can try CascadeType.ALL. It work in my case.
I'm trying to write a simple Repository test in Spring Boot. The Test code looks like this:
public class UserRepositoryTest {
private final TestEntityManager entityManager;
private final UserRepository userRepository;
#Autowired
public UserRepositoryTest(TestEntityManager entityManager, UserRepository userRepository) {
this.entityManager = entityManager;
this.userRepository = userRepository;
}
#Test
public void test() {
String firstName = "Frank";
String lastName = "Sample";
String email = "frank#example.com";
String username = "frank#example.com";
String password = "floople";
String passwordConfirm = "floople";
RegisterUserRequest registerUserRequest = new RegisterUserRequest(firstName, lastName, email, username, password, passwordConfirm);
User user = new User(registerUserRequest);
user.setSpinsRemaining(0);
userRepository.save(user);
userRepository.setSpinsRemainingToTen();
User found = userRepository.findByUsername(username);
assertThat(found.getSpinsRemaining()).isEqualTo(10);
}
What's I expect to happen is that the new User object is persisted to the database, the row in the database is modified to set spinsRemaining to 10, and then the now-modified row is retrieved from H2 and shoved into a new variable named "found". The "found" variable will point to an instance of a User object with ten spins remaining.
What actually happens is that the "found" variable points to the exact same instance of User that the "user" variable is. In fact, if I modify some property of the "user" variable AFTER persisting it to H2, the resultant "found" object also has the modified property. According to IntelliJ, both "user" and "found" are pointing to the same thing. How is that possible?
Hibernate caches entities inside a transaction in memory ("first level cache"). - Every time it retrieves an entity from database (or when it's asked to do so by the entity id) it will first look for it in cache so you don't have multiple instances of one entity with the same ID.
But in tests it's sometimes useful to have a "fresh" entity as it can uncover bugs in your persistance configuration/code. What you need to do:
Call EntityManager#flush - this will force synchronization of your changes to the database (save method does not guarantee that when called inside a transaction).
Call EntityManager#clear - Hibernate will forget about previous entity instances and will start fetching from DB again.
Alternatively: You can also instruct your Spring repository method to clear entities automatically after a modifying query. - But this will wipe out all entity instances and not only the one you are modifying so it might not be desirable in your application code.
I have a Model attribute that needs to set #ReadOnlyProperty so that it won't persist after first inserting the line.
Assume my model like below
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(updatable = false, nullable = false)
#JsonIgnore
private Long id;
#Column(unique = true, nullable = false)
#ReadOnlyProperty
private String openId;
}
then I have a UserRepository:
public interface UserRepository extends JpaRepository<User, Long> {
}
then I provide 2 Restful API for POST and PUT.
The create user operation code is as simple as below:
user.setOpenId(1);
userRepository.save(user)
The update user operation is almost the same:
user.setOpenId(2);
user = userRepository.save(user);
I'm surprised that the user's openId attribute will be changed, after POST and then PUT, the returned user object will have the changed value.(user.getOpenId() == 2)
It looks like #ReadOnlyProperty not working, I'm using the RELEASE version of spring-boot-starter-data-jpa. Can someone help explain?
It seems that #ReadOnlyProperty doesn't work. The following bug report is open for years:
Properties with #ReadOnlyProperty annotation are being nullified in PATCH requests
If you want to deny modifying the property via Spring Data Rest endpoints, use the #JsonProperty(access = Access.READ_ONLY) annotation. It affects the JSON deserialization, so the annotated property never reaches Spring Data Rest.
If you also need to deny the writing of the property via Spring Data JPA, you can use the following JPA annotation: #Column(updatable=false) It denies the override on the underlaying JPA level, instead of Spring Data JPA level.
I have a CrudRepository throug which I can access my entities. Let's say I have an entity called Report (all oversimplified and not compiling):
#Entity
public class Report{
#Id
private Long id;
private boolean classified;
private Date date;
private String reportdata;
}
And a CrudRepository:
#RepositoryRestResource(collectionResourceRel = "reports", path = "report")
public interface ReportRepository extends CrudRepository<Report, Long>
{
findByDate(Date date); // <---- I want this to return only reports which are not classified for users who do not have the appropriate role
}
The findByDate will return all reports, including all classified reports for all users making the request. I want to restrict the access to the data based on the currently authenticated user. Is this possible?
You need Spring Security 4. It now integrates with Spring Data.
http://docs.spring.io/spring-security/site/docs/4.0.2.RELEASE/reference/htmlsingle/#data
Something like:
#Repository
public interface ReportRepository extends CrudRepository<Report,Long> {
#Query("select r from Report r where r.date=?1 and r.owner.id = ?#{ principal?.id }")
Report findByDate(Date date);
}
REST is stateless. It means that the server stores NO runtime informations (session, role etc.) about client. So if you want to use REST you should generate an API key for you client. Use a simple path filter to check whether the API key valid or not.
But perhaps you mean AJAX ?