Constructor takes only ID of referenced entity, but getter returns the entity itself - possible? - spring

Let's assume, I've a simple relationship of Student and Teacher Entities
I'm using Spring Boot with kotlin-jpa plugin, so data classes should work fine:
data class Student(
#Id
#GeneratedValue(strategy = IDENTITY)
val id: Long = null,
#OneToOne(fetch = FetchType.LAZY)
var responsibleTeacher: Teacher,
// ... other props
)
data class Teacher(
#Id
#GeneratedValue(strategy = IDENTITY)
val id: Long = null,
val name: String,
// ... other props
)
My problem: To construct an instance of Student, I always need an instance of the (already persisted) Teacher as well. As I only have the ID of the teacher at hand, I first need to obtain the full Teacher entity from the database and then pass it to the constructor of Student:
val responsibleTeacher = getTeacherFromDB(teacherId)
val student = Student(responsibleTeacher)
What I would like to to, is to pass only the Teacher ID in the constructor, but still being able to query the full Teacher entity from Student when calling the getter/property.
data class Student(
#Id
#GeneratedValue(strategy = IDENTITY)
val id: Long = null,
#Column(name = "responsible_teacher_id")
private var responsibleTeacherId: Long,
// ... other props
// pseudo-code, doesn't work!
// Should query the [Teacher] Entity by [responsibleTeacherId], specified in constructor
#OneToOne(fetch = LAZY)
var responsibleTeacher:Teacher
)
I was messing around with this for almost a day but couldn't find any working solution. Is there any?

For this purpose you can use a proxy that you can retrieve by calling entityManager.getReference(Teacher.class, teacherId)

Use #JoinColumn annotation
#Column(name = "responsible_teacher_id")
private var responsibleTeacherId: Long,
#JoinColumn(name = "responsible_teacher_id")
#OneToOne(fetch = LAZY, insertable = false, updatable = false)
var responsibleTeacher: Teacher

Related

Creating subcategories in kotlin spring boot

I need to implement categories and subcategories within my entities. Here's what I have so far and think it should be:
StockCategory.kt
#Entity
#Table(name = "table_categories")
data class StockCategory(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "category_id")
val id: Long? = null,
#ManyToOne
#JoinColumn(name = "parentid")
val parent: StockCategory? = null,
#ManyToMany(mappedBy = "categories")
var stockItems: MutableList<StockItem> = mutableListOf(),
#OneToMany(mappedBy = "parent")
var childCategories: MutableList<StockCategory> = mutableListOf(),
)
StockItem.kt
#Entity
#Table(name = "table_stock")
data class StockItem(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "stock_item_id")
val id: Long? = null,
#Column(name = "stock_item_name")
var name: String = "New Item",
...
#ManyToMany
#JoinColumn(name = "item_category", referencedColumnName = "category_id")
var categories: MutableList<StockCategory> = mutableListOf(),
...
)
Now at the moment, this looks to be correct... At the very least Spring Boot is not complaining.
However, in terms of what to do next, I'm not sure. I know I need to implement a JpaRepository, of which I have the current:
StockCategoryRepository.kt
interface StockCategoryRepository: JpaRepository<StockCategory, Long> {
}
I also need to implement the relevant methods in my service class.
What exactly do I need to do next in order to get this to work and be able to use the information later on? Please also ELI5 too as although I have a decent amount of knowledge on this, I'm still not where I would like to be when it comes to this.
A few background bits if it makes it easier for you.
I'm using H2 as my database, Spring Boot and Kotlin as my language.

Why Value is not getting assigned in JPA for insert statement

Hi I have couple of Entity classes as below, using lombok for getter and setters
Parent Entity Class have
#Table(name = "PARTY")
#Entity
public class Party {
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#OneToMany(targetEntity = DVLoanParticipants.class,cascade = CascadeType.ALL)
#JoinColumn(name = "PARTY_ID")
#MapKey(name="dvpParticipantName")
#LazyCollection(LazyCollectionOption.FALSE)
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
}
Child Entity Class have
#Table(name = "DV_LOAN_PARTICIPANTS")
#Entity
public class DVLoanParticipants implements Serializable {
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#Id
#Column(name = "DVP_PARTICIPANT_NAME")
private String dvpParticipantName;
#Column(name = "DVP_PARTICIPANT_TYPE")
private String dvpParticipantType;
}
In service class i am calling save operation as
repository.save(parentEntityObject);
I am able to execute update statements ,but when i try to insert new row for child entity class i am getting an error saying
cannot insert NULL into ("ABC"."DV_LOAN_PARTICIPANTS"."PARTY_ID")
But if i print the parentEntityObject just before the save operation i see the values like
(partyId=12345678, dvpParticipantName=XYZ, dvpParticipantType=VKP)
I see the query formed as
insert
into
DV_LOAN_PARTICIPANTS
(DVP_PARTICIPANT_TYPE, PARTY_ID, DVP_PARTICIPANT_NAME)
values
(?, ?, ?)
Just before te save i am seeing valules in the Object
Builder=DVLoanParticipants(partyId=123456, dvpParticipantName=Builder,
dvpParticipantType=Individual)
Update
This is the setting part for values
DVLoanParticipants dvLoanParticipants = new
DVLoanParticipants();
dvLoanParticipants.setPartyId(Long.valueOf(partyId));
dvLoanParticipants.setDvpParticipantName("Builder");
dvLoanParticipants.setDvpParticipantType("Individual");
Party party = new Party();
Map<String, DVLoanParticipants> dvLoanParticipantsMap = new
java.util.HashMap<>();
dvLoanParticipantsMap.put("Builder", dvLoanParticipants);
party.setPartyId(Long.valueOf(partyId));
party.setDvLoanParticipantsMap(dvLoanParticipantsMap);
repository.save(party);
What is the mistake i am doing ?
The root cause of your problem in this part:
#OneToMany(targetEntity = DVLoanParticipants.class,cascade = CascadeType.ALL)
#JoinColumn(name = "LOAN_ID")
#MapKey(name="dvpParticipantName")
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
actually for your case the column name in the #JoinColumn means:
If the join is for a unidirectional OneToMany mapping using a foreign key mapping strategy, the foreign key is in the table of the target entity.
So, assuming for the clarity that you want to map the following schema:
create table PARTY
(
PARTY_ID int,
-- ...
primary key (PARTY_ID)
);
create table DV_LOAN_PARTICIPANTS
(
PARTY_ID int,
DVP_PARTICIPANT_NAME varchar(50),
DVP_PARTICIPANT_TYPE varchar(10),
-- ...
primary key (PARTY_ID, DVP_PARTICIPANT_NAME),
foreign key (PARTY_ID) references PARTY(PARTY_ID)
);
You can use the following mapping:
#Entity
#Table(name = "PARTY")
public class Party
{
#Id
#Column(name = "PARTY_ID")
private Long partyId;
// I use fetch = FetchType.EAGER instead of deprecated #LazyCollection(LazyCollectionOption.FALSE)
// targetEntity = DVLoanParticipants.class is redundant here
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "PARTY_ID") // this is DV_LOAN_PARTICIPANTS.PARTY_ID column
#MapKey(name = "dvpParticipantName")
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
public Party()
{
dvLoanParticipantsMap = new HashMap<>();
}
// getters / setters
public void addParticipant(DVLoanParticipants p)
{
this.dvLoanParticipantsMap.put(p.getDvpParticipantName(), p);
p.setPartyId(getPartyId());
}
}
#Entity
#Table(name = "DV_LOAN_PARTICIPANTS")
public class DVLoanParticipants implements Serializable
{
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#Id
#Column(name = "DVP_PARTICIPANT_NAME")
private String dvpParticipantName;
#Column(name = "DVP_PARTICIPANT_TYPE")
private String dvpParticipantType;
// getters / setters
}
and example how to save:
Party party = new Party();
party.setPartyId(2L);
// ...
DVLoanParticipants part1 = new DVLoanParticipants();
part1.setDvpParticipantName("Name 3");
part1.setDvpParticipantType("T1");
DVLoanParticipants part2 = new DVLoanParticipants();
part2.setDvpParticipantName("Name 4");
part2.setDvpParticipantType("T1");
party.addParticipant(part1);
party.addParticipant(part2);
repository.save(party);
and several notes:
The LazyCollectionOption.TRUE and LazyCollectionOption.FALSE values are deprecated since you should be using the JPA FetchType attribute of the #OneToMany association.
You use hibernate specific approach for mapping сomposite identifiers. As it's mentioned in the hibernate documentation:
The restriction that a composite identifier has to be represented by a primary key class (e.g. #EmbeddedId or #IdClass) is only JPA-specific.
Hibernate does allow composite identifiers to be defined without a primary key class via multiple #Id attributes.
But if you want to achieve more portability you should prefer one of the jpa allowed approaches.

Spring Boot JPA - find by 2 columns

I don't know how to create a JPA query to get all records from my table:
#Entity
#Table(name = "A")
public class A{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(nullable = false, updatable = false)
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "message_id")
private Message;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private User user;
}
#Entity
#Table(name="message")
#Getter
#Setter
public class Message{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(nullable = false, updatable = false)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "account_id", foreignKey = #ForeignKey(name = "FK_account_to_message"))
private Account account; //and then search by Account.id
}
SO I have 2 types of objects in A table (sometimes object is created from email, sometimes from a file):
one type without user_id (null) -> then I have to find all A object by searchning by Message -> Account -> Id
second type with user_id -> we can directly get A objects by values in user_id column
I want to get all records for specific user_id -> how to do that in most efficient way? I don't want to invoke 2 methods in repository:
User user = userService.getEmail();
List<A> aObjects= Stream.concat(ARepository.findByMessage_Account_Id(user.getId()).orElse(new ArrayList<>()).stream(),
aRepository.findByUser_Id(user.getId()).orElse(new ArrayList<>()).stream()).collect(Collectors.toList());
Is it possible to create ONE repository method that finds all records for 2 different objects (one with user_id and second without user_id)?
I guess that this query is to complex for using derived from the method name query. As it stated in the documentation:
Although getting a query derived from the method name is quite convenient, one might face the situation in which either the method name parser does not support the keyword one wants to use or the method name would get unnecessarily ugly. So you can either use JPA named queries through a naming convention or rather annotate your query method with #Query.
So, I would suggest just write the following query:
#Query("select aa from A aa left join aa.user u left join aa.message msg left join msg.account acc where (u is null and acc is not null and acc.id = :userId) or (u is not null and u.id = :userId)")
List<A> findByUserOrAccountId(#Param("userId") Long userId);

How to make IDs non-sequencial?

I have an entity
#Entity
data class Person (
#Id #GeneratedValue
val id: Long
)
But I noticed the values for id are consecutive. Is there a way to let Spring Boot make them more random?
You can create a custom identifier generator and use it.
#Entity
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq")
#GenericGenerator(
name = "seq",
strategy = "com.java.generators.SequenceIdGenerator",
parameters = {...})
private String id;
...
}
Here you should create com.java.generators.SequenceIdGenerator by own
A good tutorial about this

Spring JPA cannot map a field with custom setter in a Kotlin data class

I've got a Kotlin data class with a custom setter. The Spring JPA framework cannot seem to map the property with the custom setter. If I remove the custom getter/setter and rename the property to login instead of _login, everything seems to work fine. How can I create the property in the Kotlin data class with a custom setter, so that it is recognised in the JPA framework?
User.kt
#Entity
#Table(name = "jhi_user")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
data class User (
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
var id: Long? = null,
#NotNull
#Pattern(regexp = Constants.LOGIN_REGEX)
#Size(min = 1, max = 50)
#Column(name = "login", length = 50, unique = true, nullable = false)
var _login: String? = null,
#JsonIgnore
#NotNull
#Size(min = 60, max = 60)
#Column(name = "password_hash",length = 60)
var password: String? = null,
...
#JsonIgnore
#ManyToMany
#JoinTable(
name = "jhi_user_authority",
joinColumns = arrayOf(JoinColumn(name = "user_id", referencedColumnName = "id")),
inverseJoinColumns = arrayOf(JoinColumn(name = "authority_name", referencedColumnName = "name")))
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#BatchSize(size = 20)
var authorities: MutableSet<Authority>? = null): AbstractAuditingEntity(), Serializable {
//Lowercase the login before saving it in database
var login: String?
get() = _login
set(value) {
_login = StringUtils.lowerCase(value, Locale.ENGLISH)
}
}
The error I'm getting:
...
Caused by: java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [login] on this ManagedType [com.sample.domain.AbstractAuditingEntity]
at org.hibernate.metamodel.internal.AbstractManagedType.checkNotNull(AbstractManagedType.java:128)
at org.hibernate.metamodel.internal.AbstractManagedType.getAttribute(AbstractManagedType.java:113)
at org.hibernate.metamodel.internal.AbstractManagedType.getAttribute(AbstractManagedType.java:111)
at org.springframework.data.jpa.repository.query.QueryUtils.toExpressionRecursively(QueryUtils.java:569)
at org.springframework.data.jpa.repository.query.JpaQueryCreator$PredicateBuilder.getTypedPath(JpaQueryCreator.java:377)
at org.springframework.data.jpa.repository.query.JpaQueryCreator$PredicateBuilder.build(JpaQueryCreator.java:300)
at org.springframework.data.jpa.repository.query.JpaQueryCreator.toPredicate(JpaQueryCreator.java:205)
at org.springframework.data.jpa.repository.query.JpaQueryCreator.create(JpaQueryCreator.java:117)
at org.springframework.data.jpa.repository.query.JpaQueryCreator.create(JpaQueryCreator.java:54)
at org.springframework.data.repository.query.parser.AbstractQueryCreator.createCriteria(AbstractQueryCreator.java:111)
at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:90)
at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:78)
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.<init>(PartTreeJpaQuery.java:135)
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery$CountQueryPreparer.<init>(PartTreeJpaQuery.java:256)
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:72)
at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:103)
Using custom setters for constructor parameters like this is a bit ugly (but unfortunately the only way I am aware of doing it).
For starters JPA is going to want to register both _login and login as separate columns in your database since neither of them are #Transient. I believe your issue arises here since you have marked the _login property to map to the column "login" whereas the login property has no #Column annotation so it is trying to map to it's default value of "login" which already has the _login property mapped to it.
Therefore I think you probably want to make _login transient and only persist login (I've missed out irrelevant code for brevity and clarity):
...
#Transient
var _login: String? = null,
...
#NotNull
#Pattern(regexp = Constants.LOGIN_REGEX)
#Size(min = 1, max = 50)
#Column(name = "login", length = 50, unique = true, nullable = false)
var login: String?
get() = _login
set(value) {
_login = StringUtils.lowerCase(value, Locale.ENGLISH)
}
If this still doesn't work then I really think it's more hassle than it's worth trying to get this already slightly hacky workaround for using custom setters on constructor properties working with JPA. I would suggest instead to use a #PrePersist/#PreUpdate method to do the lowercasing for you prior to saving it to the database.

Resources