OneToMany Spring Data JDBC - java

I want to model a OneToMany Relation with Spring Data JDBC. I´ve read on this very useful blog https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates that you should use references when you want to model ToMany Reference:
Therefore any Many-to-One and Many-to-Many relationship must be modeled by just referencing the id.
So I have this scenario:
One Student can have multiple Registration. And one Registration can have exactly one Student. If you delete Registration the assigned Student should not get deleted cascading.
I ended up with this modelling:
#Data
#AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = #__(#PersistenceConstructor))
public class Registration {
private final #Id
#Wither
long registrationId;
#NotNull
private String electiveType;
#NotNull
private LocalDateTime created = LocalDateTime.now();
#NotNull
private StudentRegistrationReference studentRegistrationReference;
}
#Data
#AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = #__(#PersistenceConstructor))
public class StudentRegistrationReference {
private long student;
private long registration;
}
#Data
#AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = #__(#PersistenceConstructor))
public class Student {
private final #Id
#Wither
long studentId;
#NotNull
#Size(min = 4, max = 20)
private String userId;
#NotNull
#Min(0)
private int matriculationNumber;
#NotNull
#Email
private String eMail;
private Set<StudentRegistrationReference> studentRegistrationReferences = new HashSet<>();
}
My question is whether my modeling is correctly implemented?

You are quoting the article talking about "Many-To-X" but you talk yourself about "X-To-Many". You can model a One-To-One or a One-To-Many relationship with a direct reference, or a List/Set/Map of entities.
What you should avoid are bidirectional relationships. While you probably can make them work with the approach you are using, you really shouldn't.
Which brings us to the question: How should this model look like?
The central decision to make is how many aggregates are involved?
A Student certainly is an aggregate and the Student class is its aggregate root. It can exist on its own.
But what about Registration? I'd argue, it is probably part of the same aggregate. The delete test is a good one. If you delete a Student from the system, do the registrations of that Student still have value? Or should the disappear together with the Student?
As an exercise let's do both variants. I start with: Just one aggregate:
class Registration {
#Id private long Id;
String electiveType;
LocalDateTime created = LocalDateTime.now();
}
class Student {
#Id private long Id;
String userId;
int matriculationNumber;
String eMail;
Set<Registration> registrations = new HashSet<>();
}
With this, you would have a single repository:
interface StudentRepository extends CrudRepository<Student, Long>{}
I removed all the Lombok annotations since they aren't really relevant to the problem. Spring Data JDBC can operate on simple attributes.
If Registration and Student both are aggregates it gets a little more involved:
You need to decide which side owns the reference.
First case: The Registration owns the reference.
class Registration {
#Id private long Id;
String electiveType;
LocalDateTime created = LocalDateTime.now();
Long studentId;
}
public class Student {
#Id private long Id;
String userId;
int matriculationNumber;
String eMail;
}
Second case: The Student owns the reference
class Registration {
#Id private long Id;
String electiveType;
LocalDateTime created = LocalDateTime.now();
}
class Student {
#Id private long Id;
String userId;
int matriculationNumber;
String eMail;
Set<RegistrationRef> registrations = new HashSet<>();
}
class RegistrationRef {
Long registrationId;
}
Note that the RegistrationRef doesn't have a studentId or similar. The table assumed for the registrations property will have a student_id column.

Related

Stuck in a loop while passing RequestBody in Postman

#Entity
#Table(name = "customers")
public class Customer implements Serializable{
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private int custID;
private String custName;
#Id
private String email;
private int phone;
#OneToMany (mappedBy = "customer", fetch = FetchType.LAZY)
private List<Transaction> transaction;
#Entity
#Table(name = "transactions")
public class Transaction implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int transID;
private Date date;
private int amount;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "custID", nullable= false)
private Customer customer;
These are my entities, and I have a method:
#PostMapping("/record-transaction")
public Transaction recordTransaction(#RequestBody Transaction transaction) {
return transactionService.addTransaction(transaction);
}
But when I try to create JSON in postman, I get into a loop where while entering values for transaction, at the end I must enter the Customer object as well and when I am entering customer object at the end I again reach to enter the transaction's values. Its like a never ending loop. Help
I couldn't think of anything to do at all. My mind enters the loop itself.
Decouple your DB entities from your request/response by using an intermediate DTO.
Controller:
#PostMapping("/record-transaction")
public TransactionResponse recordTransaction(#RequestBody TransactionRequest body) {
return TransactionResponse.from(transactionService.addTransaction(
body.getDate();
body.getAmount();
body.getCustomerId();
));
}
TransactionRequest:
public class TransactionRequest {
//don't need ID here it'll be auto generated in entity
private Date date;
private int amount;
private int customerId;
}
TransactionResponse:
public class TransactionResponse {
private int id;
private Date date;
private int amount;
private int customerId;
public static TransactionResponse from(Transaction entity) {
return //build response from entity here
}
}
TransactionService:
//when your entity is lean may as well pass the values directly to reduce boilerplate, otherwise use a DTO
public Transaction addTransaction(Date date, int amount, int customerId) {
Customer customerRepo = customerRepo.findById(customerId).orElseThrow(
() -> new CustomerNotFoundException();
);
Transaction trans = new Transaction();
trans.setDate(date);
trans.setAmount(amount);
trans.setCustomer(customer);
return transactionRepository.save(trans);
}
If you want to embed the customer model inside TransactionResponse or TransactionRequest it'll be fairly easy to do and this solution will produce way nicer contract and swagger docs than a bunch of use case specific annotations in your entity.
In general decoupling you request/response payloads, service dtos and entities from each other results in code with more boilerplate but easier to maintain and without weird unexpected side effects and specific logic.

Java entity modeling with table that relates to several others

I have a doubt about how the modeling of my entity would be. Come on, I have a table in the database that serves to save documents from my system, this table has the columns id, fk_id (element foreign key), fk_table (entity name) and file_name (stores the name of my file) .
I did a lot of research before posting my question here, but I didn't find anything related to it, what would my entities, user, patient and doctor?
DB:
id
fk_id
fk_table
file_name
1
21
user
test1.jpg
2
32
doctor
test2.pdf
3
61
user
test10.pdf
4
100
patient
test5.jpg
Class:
public class User{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String LastName;
// What would a one-to-many relationship look like?
}
public class patient{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
// What would a one-to-many relationship look like?
}
You can use #Where. But be aware that #Where is a Hibernate annotation. It's not in the JPA standard.
For example in the User entity: (I assume that your table is mapped to an entity called Document)
#Where( clause = "fk_table = 'user'")
#JoinColumn(name = "fk_id")
#OneToMany
private List<Document> documents = new ArrayList<>( );
The following is based only on standard JPA annotations. The idea is to create an inheritance hierarchy for the documents table. The base is:
#Entity
#Table(name = "XX_DOCUMENT")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "fk_table")
public abstract class BaseDocument {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#Column(name = "file_name")
private String fileName;
}
Here we define that all entities extending this will go to the same table, with the fk_table column to discriminate. The entities extending it are defined as follows:
#Entity
#DiscriminatorValue("doctor")
public class DoctorDocument extends BaseDocument {
#ManyToOne
#JoinColumn(name = "fk_id")
private Doctor doctor;
}
#Entity
#DiscriminatorValue("patient")
public class PatientDocument extends BaseDocument {
#ManyToOne
#JoinColumn(name = "fk_id")
private Patient patient;
}
// and so on
The interesting thing is that we are reusing the column fk_id to point to the right table. From a small experiment, Hibernate seems to not have problems with it. I would suggest that you manage the DB creation another way just to be safe.
The Doctor, Patient etc need not have a common base class, e.g.:
#Entity
#Table(name = "XX_DOCTOR")
public class Doctor {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#OneToMany(mappedBy = "doctor")
private Collection<DoctorDocument> documents = new ArrayList<>();
// any doctor-specific fields
}
#Entity
#Table(name = "XX_PATIENT")
public class Patient {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#OneToMany(mappedBy = "patient")
private Collection<PatientDocument> documents = new ArrayList<>();
// any patient-specific fields
}
// and so on
You can read a (doctor, patient, ...)'s documents from the relevant collection. You can even query BaseDocument instances based on any criteria.
You can even go ahead and do more fabcy stuff with the Java code. E.g. define an interface HasDocuments:
public interface HasDocuments<D extends BaseDocument> {
Collection<D> getDocuments();
}
Doctor, Patient, ..., implements this, so they can all be treated the same way.

Spring Boot: POST request to entity with ManyToMany relationship

I'm working on a database for adding bands, musicians, instruments, etc.
I have a table 'band' and a table 'musician'. They have a ManyToMany relationship (one band can have many musicians, a musician can be in many bands), with an extra table BandMusician that has an embeddedId BandMusicianId. I did it like this because I want the relationship between bands and musicians to have also other information, like the year the musician joined the band.
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Band {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String genre;
private int year;
#OneToOne(mappedBy = "band")
private Website website;
#OneToMany(mappedBy = "band")
private List<Album> albuns;
#OneToMany(mappedBy = "band")
private List<BandMusician> musicians;
}
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
#JsonDeserialize(using = MusicianJsonDeserializer.class)
public class Musician {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#JsonFormat(pattern = "dd-MM-yyyy")
#JsonProperty("DoB")
#Column(name = "date_of_birth")
private LocalDate DoB;
#ManyToMany
#JoinTable(
name = "musician_instruments",
joinColumns = #JoinColumn(name = "musician_id"),
inverseJoinColumns = #JoinColumn(name = "instrument_id")
)
private List<Instrument> instruments = new ArrayList<>();
#OneToMany(mappedBy = "musician")
private List<BandMusician> bands;
public void addInstrument(Instrument instrument) {
this.instruments.add(instrument);
}
}
#Embeddable
#Data
#AllArgsConstructor
#NoArgsConstructor
public class BandMusiciansId implements Serializable{
private static final long serialVersionUID = 1L;
#Column(name = "band_id")
private Long bandId;
#Column(name = "musician_id")
private Long musicianId;
}
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class BandMusician {
#EmbeddedId
private BandMusiciansId id = new BandMusiciansId();
#ManyToOne
#MapsId("bandId")
#JoinColumn(name = "band_id")
private Band band;
#ManyToOne
#MapsId("musicianId")
#JoinColumn(name = "musician_id")
private Musician musician;
private String role;
private int joined;
}
When I receive a POST request to "/musician" I can save a musician. I'm using Jackson to deserialize a request like this:
{
"name": "John the Ripper",
"DoB": "03-12-1965",
"instruments": "voice, guitar",
"bands": "Band1, Band2"
}
With Jackson I can get each band, search with the BandRepository and create a BandMusician.
THE PROBLEM: When I receive the request, in order to create a BandMusician I have to create a BandMusiciansId, and to do that I need the bandId and the MusicianId. But I'm creating the musician right now, so I don't have the musicianId. It is created automatically when I save the musician.
MusicianJsonDeserializer class
public class MusicianJsonDeserializer extends JsonDeserializer<Musician>{
private final InstrumentRepository instrumentRepository;
private final BandRepository bandRepository;
#Autowired
public MusicianJsonDeserializer(
InstrumentRepository instrumentRepository,
BandRepository bandRepository
) {
this.instrumentRepository = instrumentRepository;
this.bandRepository = bandRepository;
}
#Override
public Musician deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JacksonException {
ObjectCodec codec = p.getCodec();
JsonNode root = codec.readTree(p);
Musician musician = new Musician();
musician.setName(root.get("name").asText());
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
musician.setDoB(LocalDate.parse(root.get("DoB").asText(), formatter));
if (root.get("instruments") != null) {
String instrumentList = root.get("instruments").asText();
String[] instrumentArray = instrumentList.split(", ");
List<Instrument> musicianInstrumentList = new ArrayList<>();
for (String instrument : instrumentArray) {
Instrument instrumentFound =
instrumentRepository.findByName(instrument)
.orElseThrow(RuntimeException::new);
// TODO custom exception
musicianInstrumentList.add(instrumentFound);
}
musician.setInstruments(musicianInstrumentList);
}
if (root.get("bands") != null) {
// TODO Stuck here!
What I thought of doing: In my MusicianService, after saving the musician, I can create the BandMusician and the relationship. I think doing this in the Service layer would be a bad choice though.
EDIT: To make it easier to understand, I created a project only with the relevant parts of this one and pushed to github (https://github.com/ricardorosa-dev/gettinghelp).
Again, what I want is to be able to send a POST to "/musician", that will be caught by the MusicianJsonDeserializer, and somehow create a BandMusicianId and BandMusician for each band sent in the request body.
I have the entities Band and Musician and a ManyToMany relationship between them with an association table BandMusician.
What I wanted was to create the entity Musician and the relationship (BandMusician) in the same request.
As far as I can gather it is not possible, because in order to create a record in the association table (BandMusician), I would have to have the musician (I'm creating in this request) already created.
I tried everything just to see if it was POSSIBLE and wasn't able to do it. But even if it was possible, it would be a very bad practice, since it would make the class too tightly coupled.
The clear solution was to create only the Musician with this request, and then send another request to create the connection between Band and Musician.
I also tried to create many entries in the BandMusician table with one request, which was also impossible, because the JsonDeserializer table doesn't seem to accept List<> as a return type. I was trying to avoid making a lot of requests to create the relationship entries (for a musician that is in five bands, for example), but it seems it is better to keep things clear and simple.
I now save one musician-band relationship per request:
{
"musician": "Awesome musician",
"band": "Awesome band",
"role": "guitar",
"joined": 2003
}

How to depict joins with #Query annotation in Spring JPA Repository method

I am using Spring-Boot with JPA and a MySQL backend. Now I got quite confused about the repositories Spring-Boot provides. I know these are quite powerful (and seem to be quite useful since they can shorten your code a lot). Still, I do not understand how to represent Joins within them, since the result-set should be a combination of specified attributes in the select of a few Entities.
Now let's assume we have three tables Book, Author, AuthorOfBook, where the last one is simply connecting Book and Author by a combined Primary key. I guess we had the following Java-Classes:
Entity Book:
#Entity
#Table(name="BOOK")
public class Book {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private int id;
#Column(name = "TITLE")
private String title;
}
Entity Author
#Entity
#Table(name="AUTHOR")
public class Author {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private int id;
#Column(name = "LASTNAME")
private String lastname;
#Column(name = "FIRSTNAME")
private String firstname;
//Let's assume some getters and setters and a constructor
}
Entity AuthorOfBook:
#Entity
#Table(name="BOOK")
public class Book {
#EmbeddedId
private AuthorOfBookId pk;
}
An Embedded ID
#Embeddable
public class AuthorOfBookId implements Serializable {
private int authorId;
private int bookId;
}
Repository
#Repository
public interface AuthorOfBookRepository extends JpaRepository<,AuthorOfBookId> {
}
Now how would I represent that query:
SELECT b.name, a.firstname, a.lastname from AuthorOfBook ab inner join Book b on b.id = ab.book_id inner join Author a on a.id = ab.author_id where a.lastname = :lastname;
in my repository? I know the signature would need to be like
#Query([the query string from above])
public (...) findAuthorAndBookByAuthorLastname(#Param("lastname") String lastname);
but I cannot make out what Type the return would be like. What is that method returning? (simply AuthorOfBook would not work I guess)
You don't want AuthorOfBook as a separate Entity. Book should have a field of type Author as a #ManyToOne relationship. That way, given any Book, you can find the author's details.
If you want to handle audits fields you can do something like this:
Audit class
#Embeddable
public class Audit {
#Column(name = "created_on")
private Timestamp createdOn;
#Column(name = "updated_on")
private Timestamp updatedOn;
#Column(name = "is_deleted")
private Boolean isDeleted;
//getters and setters
}
AuditListener to update automatically audits fields
public class AuditListener {
private Long loggedUser = 1001L;
/**
* Method to set the fields createdOn, and isDeleted when an entity is persisted
* #param auditable
*/
#PrePersist
public void setCreatedOn(Auditable auditable) {
Audit audit = auditable.getAudit();
if (audit == null) {
audit = new Audit();
auditable.setAudit(audit);
}
audit.setIsDeleted(Boolean.FALSE);
audit.setCreatedOn(Timestamp.from(Instant.now()));
}
/**
* Method to set the fields updatedOn and updatedBy when an entity is updated
* #param auditable
*/
#PreUpdate
public void setUpdatedOn(Auditable auditable) {
Audit audit = auditable.getAudit();
audit.setUpdatedOn(Timestamp.from(Instant.now()));
}
}
And add this to the entities
#EntityListeners(AuditListener.class)
public class Book implements Auditable {
#Embedded
private Audit audit;

Hibernate One-to-One DTO-Entity population

I have 2 entities in my DB with one-to-one one directional mapping:
User and PasswordResetToken. The idea behind this is to create new token each time user requests password reset and store only the latest one.
Below are my entities:
#Entity
#Table(name = "USERS")
#Getter #Setter
public class User implements Serializable {
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.AUTO, generator = "usersSeq")
#SequenceGenerator(name = "usersSeq", sequenceName = "SEQ_USERS", allocationSize = 1)
private long id;
#Column(name = "NAME")
private String name;
#Column(name = "PASSWORD")
private String password;
#Column(name = "EMAIL")
private String email;
#Column(name = "ROLE")
private Integer role;
}
///...
#Entity
#Table(name = "PASSWORD_RESET_TOKENS")
#Getter
#Setter
public class PasswordResetToken implements Serializable {
private static final int EXPIRATION = 24;
#Column(name = "TOKEN")
private String token;
#Id
#OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(nullable = false, name = "user_id")
private User user;
#Column(name = "EXPIRY_DATE")
private Instant expiryDate;
public PasswordResetToken() {
}
public void setExpiryDate(ZonedDateTime expiryDate) {
this.expiryDate = expiryDate.plus(EXPIRATION, ChronoUnit.HOURS).toInstant();
}
}
Also, I have DTOs created for both of them to pass them around my app.
Code snippets:
#Getter #Setter
public class PasswordResetTokenModel {
private String token;
private ZonedDateTime expiryDate;
private UserModel user;
}
UserModel is also used for Spring Security
#Getter
#Setter
public class UserModel extends User {
public UserModel(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
private long id;
private String name;
public String getEmail() {
return this.getUsername();
}
}
For population I've created 2 populators:
#Component
public class UserPopulatorImpl implements UserPopulator {
#Autowired
UserDetailsService userDetailsService;
#Override
public UserModel populateToDTO(User user) {
UserModel userModel = new UserModel(user.getEmail(), user.getPassword(), userDetailsService.getAuthorities(user.getRole()));
userModel.setId(user.getId());
return userModel;
}
#Override
public User populateToDAO(UserModel userModel) {
User user = new User();
user.setEmail(userModel.getEmail());
user.setName(userModel.getName());
user.setPassword(userModel.getPassword());
//TODO: change it!
user.setRole(1);
return user;
}
}
//...
#Component
public class PasswordResetTokenPopulatorImpl implements PasswordResetTokenPopulator {
#Autowired
UserPopulator userPopulator;
#Override
public PasswordResetTokenModel populateToDTO(PasswordResetToken passwordResetToken) {
PasswordResetTokenModel passwordResetTokenModel = new PasswordResetTokenModel();
passwordResetTokenModel.setUser(userPopulator.populateToDTO(passwordResetToken.getUser()));
passwordResetTokenModel.setToken(passwordResetToken.getToken());
passwordResetTokenModel.setExpiryDate(ZonedDateTime.ofInstant(passwordResetToken.getExpiryDate(), ZoneId.systemDefault()));
return passwordResetTokenModel;
}
#Override
public PasswordResetToken populateToDAO(PasswordResetTokenModel passwordResetTokenModel) {
PasswordResetToken passwordResetToken = new PasswordResetToken();
passwordResetToken.setExpiryDate(passwordResetTokenModel.getExpiryDate());
passwordResetToken.setUser(userPopulator.populateToDAO(passwordResetTokenModel.getUser()));
passwordResetToken.setToken(passwordResetTokenModel.getToken());
return passwordResetToken;
}
}
I'm saving object using
sessionFactory.getCurrentSession().saveOrUpdate(token);
When I use this code, I'm getting following exception
object references an unsaved transient instance - save the transient instance before flushing: com.demo.megaevents.entities.User
There are currently 2 issues in this code:
Seems like Cascade.ALL in my OneToOne mapping is not working. If
I create separate primary key in Token class everything works almost
as expected but storing every created token in DB (more like
OneToMany relation), however I want to avoid it as I need to store
only one token per user in my DB
I don't like using new in populators, as it forces hibernate to create new object while flushing session. However, I also don't want to do another select to fetch this data from DB because just before mentioned populator I already do this query to fetch it and I think that it's an overhead.
Also, I really want to have DTOs and I don't want to remove DTO layer.
So, my questions:
What is the correct way to handle population between DTO and entities?
Are there any other improvements (probably architectural) to my solution?
Thanks a lot.
I'm not sure why you would let UserModel extend User, but I guess you did that because you didn't want to have to copy all properties from User into UserModel. Too bad, because that's what is going to be needed to have a clean separation between the entity model and data transfer model.
You get that exception because you try to persist a PasswordResetToken that has a reference to a User object with an id, but the User isn't associated with the current session. You don't have to query the user, but at least association it with the session like this:
PasswordResetToken token = // wherever you get that from
Session s = sessionFactory.getCurrentSession();
token.setUser(s.load(User.class, token.getUser().getId());
s.persist(token);
Cascading would cause the User to be created/inserted or updated via a SQL INSERT or UPDATE statement which is apparently not what you want.
You could do the Session.load() call in you populators if you want, but I'd not do that. Actually I would recommend not having populators at all, but instead create the entity objects in your service instead.
Normally you only have a few(mostly 1) ways of actually creating a new entity object, so the full extent of the transformation from DTO to entity will only be relevant in very few cases.
Most of the time you are going to do an update and for that, you should first select the existing entity and apply the fields that are allowed to be changed from the DTO on the entity object.
For providing the presentation layer with DTOs I would recommend using Blaze-Persistence Entity Views to avoid the manual mapping boilerplate and also improve performance of select queries.

Categories

Resources