I'm building an Spring Boot application which allows registering, submitting of various data and etc which requires validation. For example I have this entity setup with basic constraints:
#Entity
public class Account extends BaseEntity {
public static final int MAX_LENGTH = 64;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private String username;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private String email;
...
}
Each time before creating a new account, validation is performed on the service layer:
public Account register(String username, String email, String rawPassword) {
if (!accountValidator.validate(username, email, rawPassword)) {
return null;
}
Account account = new Account(username, email, passwordEncoder.encode(rawPassword));
try {
return accountRepository.save(account);
} catch (DataIntegrityViolationException e) {
return null;
}
}
The validator snippet:
public boolean validate(String username, String email, String password) {
if (!validateUsernameStructure(username)) {
return false;
}
if (!validateEmailStructure(email)) {
return false;
}
if (!validatePasswordStructure(password)) {
return false;
}
// Performs a query on all users to see if no such email or username
// already exists. Before checking the email and username are set to
// lowercase characters on the service layer.
if (accountService.doesEmailOrUsernameExist(email, username)) {
return false;
}
return true;
}
Now in this case if I get a lot of multiple requests and one of them manages to get past the validation, I will encounter an exception if the username or email is forced to be lowercase in the first place. But for example I want to allow users to register with upper case/lower case username, email characters and etc. Based on this question I could add a additional field to my entity or add a more complex constraint on the database level, however I want to do this without overflow data and in java code.
So for example I get these two requests to create a new account milliseconds apart:
Request 1:
username=foo
email=foo#foo.com
Request 2:
username=foO
email=foO#foO.com
During this phase I check for duplicates (email and username is set to lower case, however when saving I keep the case intact):
if (accountService.doesEmailOrUsernameExist(email, username)) {
return false;
}
However since the requests are so close to each other, the first request might not have created a new account yet so the check for the second account passes and thus I get two database entries.
So the actual question would be, how to perform thread safe validation for these kinds of actions in my service layer without huge performance impact?
Solution I've chosen for this example
When setting the username and email, also apply those values to their lowercase counterparts and apply unique constraints on them as well. This way I get to keep the user set case for those two properties:
#Entity
public class Account extends BaseEntity {
public static final int MAX_LENGTH = 64;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private final String username;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private String email;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private final String normalizedUsername;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private String normalizedEmail;
public Account(String username, String email) {
this.username = username;
this.email = email;
this.normalizedUsername = username.toLowerCase();
this.normalizedEmail = email.toLowerCase();
}
...
}
There is no simple solution without the help of database because transactions are isolated from each other.
The alternative would be to temporarily store usernames/emails in memory and do complex synchronizations to perform the checks. Things would be even more complicated in clustered environments. This is very ugly and hard to maintain (and may impact performance significantly if the lock for single synchronization point is held for too long).
The standard and straightforward solution is to use database constraints.
Related
JDK 17
SpringBoot latest
JPA latest
MySQL 8.0.31
I am trying to implement a strategy that makes sure that both the name and the email address of each user are unique.
User entity:
#Entity
public class User {
......
#EmbeddedId
protected UserId id;
......
}
User id:
#Embeddable
public class UserId implements Serializable {
#Serial
private static final long serialVersionUID = -622156255674132106L;
#Column(name = "name", nullable = false)
protected String name = "";
#Column(name = "email", nullable = false)
protected String email = "";
public UserId(String name, String email) {
setName(name);
setEmail(email);
}
public UserId() {}
public void setName(String name) {
this.name = name;
}
public String getName() {
return Objects.requireNonNullElse(name, "");
}
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return Objects.requireNonNullElse(email, "");
}
}
Now, by default, it is marked as a conflict only if userA.name == userB.name && userA.email == userB.email, which means there can be two users having the same email address as long as they do not share one single name. How to stop this from happening? What I expect is userA.name == userB.name || userA.email == userB.email.
I've tried overriding equals() and hashcode() in the following way.
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserId userId)) return false;
if (Objects.equals(name, userId.name)) return true;
return Objects.equals(email, userId.email);
}
#Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (email != null ? email.hashCode() : 0);
return result;
}
However, it does not work. Also, breakpoints inside these two functions are not reached.
==========Edited==========
I've solved the original problem. But when it comes to UsersRepository.existsById(), it's still considered not to exist if either of the two columns does not match. How can I fix it?
Whether you do this via annotations and schema generation, or just by creating / modifying the schema directly, the answer is the same.
You will need to create a single unique constraint in the database naming both columns, not two separate constraints.
If you want a schema generation annotation to do this, supply the #UniqueConstraint annotation to the #Table annotation, e.g.
#Table(uniqueConstraints = {
#UniqueConstraint(columnNames = {
"name", "email"
})
})
public class UserId implements Serializable {
#Serial
private static final long serialVersionUID = -622156255674132106L;
#Column(name = "name", nullable = false, unique=true)
protected String name = "";
#Column(name = "email", nullable = false, unique=true)
protected String email = "";
I am creating a new endpoint in this API, which accepts a request containing either a username or a 5 or 9-digit phone number. I need to add validations to validate the request to contain either a username OR phone number; if both are provided then return a validation error. Here is the current code I have.
#GetMapping("/users")
#Operation(summary = "API to return users based on username or phone number")
public ResponseEntity<List<UserResponseObject>> usersByNameOrNumber(
#Valid #RequestParam(name = "phone-number", required = false) #Pattern(regexp = "^(\\d{5}|\\d{9})$") String userPhoneNumber,
#Valid #PathVariable(name = "user-name", required = false) #Pattern(regexp = "^[a-zA-Z ]*$") String userName) {
try {
...
} catch (Exception e) {
...
}
}
How can I modify this block to meet the requirements? Currently, both fields are tagged as not required and there is no validation in place. I'd like it require ONLY one of these fields to be not null.
Try use optional, i dont know how solution you need.
#GetMapping("/users")
#Operation(summary = "API to return users based on username or phone number")
public ResponseEntity<List<UserResponseObject>> usersByNameOrNumber(
#Valid #RequestParam(value = "phoneNumber", required = false) #Pattern(regexp = "^(\\d{5}|\\d{9})$") Optional<String> phoneNumber,
#Valid #RequestParam(value = "userName", required = false) #Pattern(regexp = "^[a-zA-Z ]*$") Optional<String> userName) {
try {
if(phoneNumber.isPresent()){}
if(userName.isPresent()){}
} catch (Exception e) {
}
}
But i'm not sure the #Pattern and Optional will be work good, but probably yes.
Maybe is help, if i good understend your problem.
How can I check the DB if a record already exists for a given case using Spring JPA query using one params. If it exists it does an update or else it inserts as a new record. I have tried a simple implementation, but I end up with a 500 server error no other error is logged.
Resolved [java.lang.NullPointerException] Completed 500
INTERNAL_SERVER_ERROR
This is what I have tried so far
My Controller
#RequestMapping(path="/updatedm",method = RequestMethod.POST)
public Boolean updateDMStatus(#RequestParam("country") String country,
#RequestParam("Id") String pId,
#RequestParam("case") String case,
#RequestParam("status") String status,
#RequestParam("updatedBy") String updatedBy){
Boolean createDm = eaDataStoreService.updateDM(country,Id,case,status,updatedBy);
return createDm;
}
My repository
public interface DMValidatedRepository extends CrudRepository<DMValidated, String> {
DMValidated findByCase(#Param("case") String case);
}
My Service
public boolean updateDM(String country, String Id, String case, String status,String updatedBy) {
DMValidated document = dmValidated.findByCase(case);
if(document != null){
document.setStatus(status);
document.setUpdatedBy(updatedBy);
dmValidated.save(document);
}else{
document.getId();
document.getCase();
document.getCountry();
document.getStatus();
document.getUpdatedBy();
dmValidated.save(document);
}
return true;
}
My Model
#Data
#ToString
#Entity
#Table(name = "DMStatus")
public class DMValidated{
#Id
#GeneratedValue
private String id;
#Column(name = "country")
private String country;
#Column(name = "Id")
private String Id;
#Column(name = "Case")
private String case;
#Column(name = "status")
private String status;
#Column(name = "updatedBy")
private String updatedBy;
public DMValidated( String country, String Id,
String case, String status, String updatedBy){
this.country = country;
this.Id=Id;
this.case = case;
this.status =status;
this.updatedBy = updatedBy;
}
Am not sure if this is the right way of doing this, have tried to research but I have not found something concreate. How can I achieve this?
It's not difficult you have just forgotten the code to create properly the object when it is new and needs to be inserted
if(document != null){
document.setStatus(status);
document.setUpdatedBy(updatedBy);
}else{
document = new DMValidated();
document.setId(Id);
document.setCase(case);
document.setCountry(country);
document.setStatus(status);
document.setUpdatedBy(updatedBy);
}
dmValidated.save(document);
The error that occurred previously in your code is the following
}else{
document.getId();
...
}
In this else you get only when document == null, so when you invoke document.getId() a null pointer exception is thrown, and then 500 error occurs.
else{
document.getId();
document.getCase();
document.getCountry();
document.getStatus();
document.getUpdatedBy();
dmValidated.save(document);
}
above case document object initialize but property datatype string is always null
Thats why each time document.getId() is null or other property to get occurred Nullpointer.
Correct code
else{
document = new DMValidated();
document.setId(Id);
document.setCase(case);
document.setCountry(country);
document.setStatus(status);
document.setUpdatedBy(updatedBy);
}
This operation you are attempting to do is colloquially called UPSERT, and happens to be a bit of challenge to achieve purely with JPA and Hibernate. The way I've done it in the past is with jOOQ.
That said, there are good resources here in StackOverflow that will help you find an answer faster if you search for those keywords.
Anyhow, here are some readings you may want to go over first:
https://www.jooq.org/doc/3.1/manual/sql-building/sql-statements/insert-statement/insert-on-duplicate-key/
https://www.depesz.com/2012/06/10/why-is-upsert-so-complicated/
https://vladmihalcea.com/jooq-facts-from-jpa-annotations-to-jooq-table-mappings/
UPSERT in PostgreSQL using jOOQ
Pretty much any reading from Vlad Mihalcea will give you insights of this topic, plus JPA and Hibernate in general.
I'm trying to use a regex to see if an url matches the criteria I have set for it. The problem I have is that I want the regex to be defined beforehand (in the frontend) and saved in aan object. I suspect that's where it goes wrong and it won't be able to compile the Pattern correctly. Let me give an example:
String url = "https://www.website-example.com/regex/80243/somemorechars"
if (Pattern.compile("regex/\\d{5}").matcher(url).find()) {
System.out.println("Url found");
}
The above example will print "Url found" and there won't be any problems.
Now for the example that gives me the problems:
// Imagine the below object that will be created at my Angular frontend, send to the backend and saved:
#Entity()
#Table(name = "REGEXOBJ")
public class RegexObj{
#Id
#GeneratedValue(generator = "regex_gen")
#TableGenerator(name = "regex_gen", table = "ama_sequence", pkColumnValue = "Regex")
#Column(name = "ID")
private Long id;
// some other fields I need
#Column(name = "REGEX", nullable = false)
private String regex;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
// getters and setters for the other fields
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
}
// =================================================== //
public boolean checkUrl(RegexObj regexObj) {
String url = "https://www.website-example.com/regex/80243/somemorechars"
if (Pattern.compile(regexObj.getRegex()).matcher(url).find()) {
System.out.println("Url found");
}
}
Now the url won't be matched to the regex, problably because the String is 'flat' and the '\\d{5}' part won't be seen as regex anymore. Is there a way around this to make it work?
EDIT - Added a simplified version of the class I use
I fixed the issue. The problem occured in fact while saving the object that was sent from the frontend. Where I set the string as "regex/\\d{5}" via an input field at the frontend it got saved as "regex/\\\\d{5}". Changing it to "regex/\d{5}" in my input field fixed the issue!
I have this simple login-registration functionality using the following classes.
User class:
public class User implements Serializable {
private int userId;
private String username;
private String password;
public User() {
userId = 0;
username = "";
password = "";
}
// public set-get methods here
}
Verification class:
public class Verification implements Serializable {
private int userId;
private String code;
private long expirationDate; // millis
public Verification() {
userId = 0;
code = "";
expirationDate = 0;
}
public void setUserId(int Id) {
userId = Id;
}
public void setUserId(User user) {
userId = user.getUserId();
}
// the rest of set-get methods here
}
I would like to confirm if the relationship above is considered Dependency? or Association?
Also, I'm using this classes in ORM. Does omitting one setUserId() overloaded method mess up the relationship between(if there's any)? Thank you.
I would say a dependency exists between Verification and User, as you can pass a User reference to a method of Verification (it uses that reference and depends on it), but Verification instance does not OWN a reference to a User instance, instead it consumes one.
The example you gave , describes dependency .
For reference you can visit the link given below -:
https://nirajrules.wordpress.com/2011/07/15/association-vs-dependency-vs-aggregation-vs-composition/