I have a little problem to understand how it make sense to insert a List of languages in JPA for a Oracle 19 DB.
The API receives a DTO with a list of chosen languages for one user.
{
"userId": 173125,
"logname": "testor2",
"lastname": "Testimator2",
"firstname": "Testor2",
"selectedLang": [
"German",
"English"
],
}
The Table looks like this for User and had a Table with pk and fk for user and language code.
The Table should remove unused languages that not in the received DTO || insert new languages in the table that not exist.
Tables that used in the db are:
USER
Columnname
Type
keys
USERID
int
PK
LOGNAME
row
-
NAME
varchar
-
LASTNAME
varchar
-
USERLANGUAGE
Columnname
Type
keys
USERLANGUAGEID
int
PK --part of primary key
USERID
int
PK --part of primary key
Languagecode
varchar
-
Now the tricky part. I want to store the languages as a List and have thought that jpa deals with the update, remove or inserts itself.
#Entity
#Table(name = "user", schema = "test")
public class UserEntity {
#Id
#Column(name = "USERID", nullable = false)
private long userid;
#Column(name = "LOGNAME", nullable = false, length = 50)
private String logname;
#Column(name = "NAME", nullable = false, length = 60)
private String name;
#Column(name = "LASTNAME", nullable = false, length = 80)
private String lastname;
#OneToMany(mappedBy = "userid", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserlanguageEntity> languages;
public List<UserlanguageEntity> getLanguages() {
return languages;
}
public void setLanguages(List<UserlanguageEntity> languages) {
this.languages = languages;
}
public UserEntity addLanguage(UserlanguageEntity userlanguageEntity) {
languages.add(userlanguageEntity);
userlanguageEntity.setUserid(this);
return this;
}
public UserEntity removeLanguage(UserlanguageEntity userlanguageEntity) {
languages.remove(userlanguageEntity);
userlanguageEntity.setUserid(null);
return this;
}
}
UserLanguageEntity
#Entity
#Table(name = "USRLANGUAGE")
public class UsrlanguageEntity {
#EmbeddedId
private UserId user;
private String languagecode;
#Basic
#Column(name = "LANGUAGECODE", nullable = true, length = 4)
public String getLanguagecode() {
return languagecode;
}
public void setLanguagecode(String languagecode) {
this.languagecode = languagecode;
}
public UserId getUser() {
return user;
}
public void setUser(UserId user) {
this.user = user;
}
...
}
Embedded ID
#Embeddable
public class UserId implements Serializable {
#SequenceGenerator(name = "userlanguageid", sequenceName =
"USERLANGUAGEID", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"userlanguageid")
#Column(name = "USERLANGUAGEID", nullable = false, precision = 0)
private long userlanguageid;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "USERID")
private UserEntity userid;
...
}
So the fetch method is fine and works as intended, but when i want to update the languages I get errors.
First of all get the list out of db to an UserEntity, then want to clear the UserLanguageEntitylist -> the list in the UserEntity. And than add the received list, maybe thats imposible. So what would be the best implementation to get this to work. First i want to delete all languagecodes in the db and write the new list into it, but it feels a bit dirty to me because maybe some have not to be changed.
public void setUserDataForCompany(UserDto userDto) {
CompanyEntity companyEntity = companyRepository.findById(41).orElseThrow(
NoSuchElementException::new);
UserEntity userEntity = userRepository.findById(userDto.getUserId()).orElseThrow(
NoSuchElementException::new);
companyUserDataMapper.toEntity(userDto, companyEntity, userEntity);
setLanguagesForUser(userDto, userEntity);
userRepository.save(userEntity);
}
// Here i have no clue to update existing or store missing languages
private void setLanguagesForUser(UserDto userDto, UserEntity userEntity) {
userDto.getSelectedLang().forEach(language -> {
UserlanguageEntity userlanguageEntity = new UserlanguageEntity();
userlanguageEntity.setLanguagecode(language.getLngCode());
userEntity.addLanguage(userlanguageEntity);
});
}
Could someone get me a hint to doing it the right way?
Thanks a lot and cheers
Related
I am still working on my very first solo spring boot project. It is suppose to be a Rest API using the MariaDB example database Nation. There is the country_languages table which receives two foreign keys that also are the primary keys and has another regular field. First foreign key is the id from countries table and the second one is the id from languages table. When I use the save() method in order to create a new tuple I get this error:
org.springframework.dao.InvalidDataAccessApiUsageException: Provided id of the wrong type for class me.givo.nationdbapiproject.model.CountryLanguages. Expected: class me.givo.nationdbapiproject.model.CountryLanguagesId, got class java.lang.Integer; nested exception is java.lang.IllegalArgumentException: Provided id of the wrong type for class me.givo.nationdbapiproject.model.CountryLanguages. Expected: class me.givo.nationdbapiproject.model.CountryLanguagesId, got class java.lang.Integer
This is the country_languages table from the MariaDB example:
create table country_languages(
country_id int,
language_id int,
official boolean not null,
primary key (country_id, language_id),
foreign key(country_id)
references countries(country_id),
foreign key(language_id)
references languages(language_id)
);
I am using an #Embeddable class CountryLanguagesId in order to make a composite key as I found in this reference.
#Embeddable
public class CountryLanguagesId implements Serializable {
#Column(name = "country_id")
private Integer countryId;
#Column(name = "language_id")
private Integer languageId;
public CountryLanguagesId() {
}
public CountryLanguagesId(Integer countryId, Integer languageId) {
this.countryId = countryId;
this.languageId = languageId;
}
// + getters and setters
After that I created the entity for the country_languages table and its repository:
#Entity
#Table(name = "country_languages")
public class CountryLanguages {
#EmbeddedId
CountryLanguagesId countryLanguagesId = new CountryLanguagesId();
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#MapsId("countryId")
#JoinColumn(name = "country_id")
private Countries countries;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#MapsId("languageId")
#JoinColumn(name = "language_id")
private Languages languages;
#Column(name = "official", length = 1, nullable = false)
private Integer official;
public CountryLanguages() {
}
public CountryLanguages(Countries country, Languages language, Integer official) {
this.countries = country;
this.languages = language;
this.official = official;
}
// + getters and setters
#Repository
public interface ICountryLanguagesJpaRepository extends JpaRepository<CountryLanguages, Integer> {
}
There are the countries and languages entities:
#Entity
#Table(name = "countries")
public class Countries {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "country_id", length = 11, nullable = false)
private Integer countryId;
#Column(name = "name", length = 50, nullable = true)
private String name;
#Column(name = "area", nullable = false)
private BigDecimal area;
#Column(name = "national_day", nullable = true)
private java.sql.Date nationalDay;
#Column(name = "country_code2", length = 2, nullable = false)
private String countryCode2;
#Column(name = "country_code3", length = 3, nullable = false)
private String countryCode3;
#OneToMany(mappedBy = "countries", cascade = CascadeType.ALL)
private Set<CountryLanguages> countryLanguages;
public Countries() {
}
public Countries(String name, BigDecimal area, Date nationalDay, String countryCode2, String countryCode3) {
this.name = name;
this.area = area;
this.nationalDay = nationalDay;
this.countryCode2 = countryCode2;
this.countryCode3 = countryCode3;
}
#Entity
#Table(name = "languages")
public class Languages {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "language_id", length = 11, nullable = false)
private Integer languageId;
#Column(name = "language", length = 50, nullable = false)
private String language;
#OneToMany(mappedBy = "languages", cascade = CascadeType.ALL)
private Set<CountryLanguages> countryLanguages;
public Languages() {
}
public Languages(String language) {
this.language = language;
}
public Integer getLanguageId() {
return languageId;
}
These are the entries I do when get the error:
#DataJpaTest
#AutoConfigureTestDatabase(replace = Replace.NONE)
public class ICountryLanguagesJpaRepositoryTest {
#Autowired
private ICountriesJpaRepository countries;
#Autowired
private ILanguagesJpaRepository languages;
#Autowired
private ICountryLanguagesJpaRepository repository;
#Test
public void shouldSaveAndRemoveContinents() {
Countries patu = new Countries("Patu", new BigDecimal(67822.34), new Date(12321233232L), "PU", "PTU");
countries.save(patu);
Languages patuano = new Languages("Patuano");
languages.save(patuano);
CountryLanguages pLanguages = new CountryLanguages(patu, patuano, 0);
repository.save(pLanguages);
assertEquals(1, repository.findAll().size());
System.out.println(repository.findAll());
repository.deleteById(1);
assertEquals(0, repository.findAll().size());
}
I am doing this using a H2 database. Here is the complete debug console output. Sorry but cant paste it here due characters limitation.
Thanks!
Your repository definition is wrong. You should specify the embeddable type as primary key type:
#Repository
public interface ICountryLanguagesJpaRepository extends JpaRepository<CountryLanguages, CountryLanguagesId> {
}
I am trying to write the following query outlined here on sqlfiddle in JPA. I first tried using the #query annotation with native = true and that does work, but my issue is that I want the query to be more dynamic, because it could be the case where I don't want to add the clause to filter by name or by account.
My entities look something like this:
#Entity
#Table(name = "INSTRUCTION")
public class Instruction {
#Id
#Column(name = "ID", nullable = false, unique = true)
public Long id;
#Column(name = "ACCOUNT", nullable = false)
public String account;
#Column(name = "NAME", nullable = false)
public String name;
#OneToMany(cascade = CascadeType.All, fetch = FetchType.EAGER)
#JoinColumn(name = "INSTRUCTION_ID", referenceColumnName = "ID")
#OrderBy("lastUpdated")
private List<Audit> auditItems = new ArrayList<>();
//Getters & Setters
}
.
#Entity
#Table(name = "AUDIT")
public class Audit {
#Id
#Column(name = "ID", nullable = false, unique = true)
public Long id;
#Column(name = "INSTRUCTION_STATUS", nullable = false)
public InstructionStatus status;
#Column(name = "LAST_UPDATED", nullable = false)
public LocalDateTime lastUpdated;
#Column(name = "LAST_UPDATED_BY", nullable = false)
public String lastUpdatedBy;
//Getters & Setters
}
I had looked into using specifications to do this, and I managed to break my query into different specifications like so:
private Specification<Instruction> hasAccount(String account) {
return (root, query, criteriaBuilder) -> criteriaBuilder.in(root.get("account")).value(account);
}
private Specification<Instruction> havingStatus(List<String> status) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
final Subquery<Audit> auditSubquery = query.subquery(Audit.class);
final Root<Audit> audit = auditSubquery.from(Audit.class);
//select instruction id from audit where status is not in {status}
auditSubquery.select(audit.get("instruction").get("id"));
auditSubquery.where(criteriaBuilder.trim(audit.get("status")).in(status).not());
//Select instruction from table where
predicates.add(root.get("id").in(auditSubquery).not());
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
};
}
// Other specifications....
And these work fine when called like so:
final List<Instruction> instructions = this.instructionRepository.findAll(
where(havingStatus(statuses)
.and(hasAccount(account))));
But my goal is have it so that for example I could check if account == null then do not include the hasAccount specification, and so on for other fields that may be null. Is there a way I can do this?
This should do the trick.
Specification spec = where(null);
if (statuses != null) {
spec = spec.and(havingStatus(statuses))
}
if (account != null) {
spec = spec.and(hasAccount(account))
}
final List<Instruction> instructions = this.instructionRepository.findAll(spec);
I have a JPA/Hibernate mapping as follows, in one of my projects where I use spring-data-jpa in the persistence layer.
#Entity
public class SalesOrder {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
#OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, cascade = CascadeType.ALL)
#JoinColumn(name = "salesOrderId", referencedColumnName = "id", nullable = false)
List<OrderLine> lines = new ArrayList<>();
//...
}
#Entity
#IdClass(SalesOrderLinePK.class)
public class SalesOrderLine {
#Id
String sku;
#Id
#Column(name = "salesOrderId", insertable = false, updatable = false)
Integer salesOrderId;
//...
}
public class SalesOrderLinePK implements Serializable {
String sku;
Integer salesOrderId;
OrderLinePK() {}
OrderLinePK(String sku, Integer salesOrderId) {
this.sku = sku;
this.salesOrderId= salesOrderId;
}
}
When I persist a new Order entity with some new OrderLine children, it gives me an error due to the reason salesOrderId field of SalesOrderLine is not set, instead, be null.
The following is the particular test case which fails with the error.
#Test
void create() {
SalesOrder item = randomItem(false);
item = salesOrderService.create(item);
assertNotNull(item);
assertNotNull(item.getId());
assertNotNull(item.getVersion());
}
private SalesOrder randomItem(boolean persistant) {
String random = RandomString.make(5);
SalesOrder order = new SalesOrder();
order.setOrderDate(LocalDate.now());
order.setOrderState(OrderState.DONE);
for(int i = 0; i < 5; i++) {
SalesOrderLine item = new SalesOrderLine();
item.setIdx(i);
item.setCode(random);
item.setName("Catalog Item " + random);
item.setListedPrice(BigDecimal.valueOf(120.50));
item.setDiscount(BigDecimal.valueOf(10));
item.setSellingPrice(BigDecimal.valueOf(108.50));
order.getOrderLines().add(item);
}
if (persistant) {
order = salesOrderRepository.save(order);
}
return order;
}
I cannot figure out what I'm doing wrong here. I don't want it to make bidirectional either. Could you please help me solve this issue.
Imagine there is a Work table in Oracle database, the primary key of which is auto-generated sequence. There is another table called External_Reference. They are of One-to-Many relationship, i.e., one work may have many external references.
External_Reference has a foreign key Work_ID to table Work's primary key ID.
With Hibernate, wonder if possible to use saveOrUpdate(object) to save a work saveOrUpdate(aWork), which will automatically save all of its external references?
Work work = new Work("Get started with Hibernate");
Set<ExternalReference> externalRefs = new HashSet<>();
ExternalReference ref1 = new ExternalReference("isbn", "23423454");
ref1.setWork(work); // work.getId() returns null before work being saved.
externalRefs.add(ref1);
ExternalReference ref2 = new ExternalReference("doi", "d2342-345553");
ref2.setWork(work);
externalRefs.add(ref2);
work.setExternalReferences(externalRefs);
// ORA-01400: cannot insert NULL into ("External_Reference"."WORK_ID")
aHibernateSession.saveOrUpdate(work);
The challenge is, before saving the work, you won't be able to know the auto-generated work ID, which means you cannot assign it to the work's external references.
Yes you can save the work without any external references first to get the auto-generated work ID, and then assign the work ID to all of its external references and save them. But I prefer not to do a two-steps thing to save a single work, if possible.
Model classes:
#Entity
#Table(name = "Work")
public class Work implements java.io.Serializable {
#Id
#Column(name = "ID", unique = true, nullable = false, precision = 22, scale = 0)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "WORK_SEQ")
#SequenceGenerator(name = "WORK_SEQ", sequenceName = "WORK_SEQ", allocationSize = 1, initialValue = 1)
public BigDecimal getId() {
return this.Id;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "work", cascade = CascadeType.ALL, orphanRemoval=true)
public Set<ExternalReference> getExternalReferences() {
return this.externalReferences;
}
}
#Entity
#Table(name = "External_Reference")
public class ExternalReference implements java.io.Serializable {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "WORK_ID", nullable = false, insertable = false, updatable = false)
public Work getWork() {
return this.work;
}
}
Here you need to pass just
ref1.setWorkId(work);
ref2.setWorkId(work);
also, check the hibernate mapping check mapping example
//Work Class is Parent
#Entity
#Table(name = "Work")
public class Work {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "ID", unique = true, nullable = false)
private Long id;
#Column(name = "TITTLE", nullable = false)
private String title;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "work")
private Set<ExternalReference> externalReference;
public Work() {
}
public Work(String title) {
this.title = title;
}
//setter and getter
}
//ExternalReference Class is Child
#Entity
#Table(name = "External_Reference")
public class ExternalReference {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "ID", unique = true, nullable = false)
private Long id;
#Column(name = "Ref_Source", nullable = false)
private String ref_source;
#Column(name = "Ref_Id", nullable = false)
private String ref_id;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name ="Work_ID")
private Work work;
public ExternalReference() {
}
public ExternalReference(String ref_source, String ref_id) {
this.ref_source = ref_source;
this.ref_id = ref_id;
}
//setter and getter
}
The workaround I thought of and tested is a manual way (for your reference):
For a new work, manually get the next sequence from the database first, and assign it to the work and all of its external references. Then you can persist the work with all of its external references by just using aHibernateSession.saveOrUpdate(work);
For an existing work, just use its existing ID, and assign it to its new external references if any.
Getting sequences from the database will guarantee that the sequence is unique. It is thread safe.
public BigDecimal getNextSeq() {
Session session = null;
try {
session = sessionFactory.openSession(); // sessionFactory should have been initialised.
Query query = session.createSQLQuery("select WORK_SEQ.nextval as num from dual")
.addScalar("num", StandardBasicTypes.BIG_DECIMAL);
BigDecimal nextSeq = (BigDecimal) query.uniqueResult();
return nextSeq;
} finally {
if (session != null){
session.close();
}
}
}
For this workaround to work, you need to comment out the Work's key generator, as below:
#Entity
#Table(name = "Work")
public class Work implements java.io.Serializable {
#Id
#Column(name = "ID", unique = true, nullable = false, precision = 22, scale = 0)
// #GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "WORK_SEQ")
// #SequenceGenerator(name = "WORK_SEQ", sequenceName = "WORK_SEQ", allocationSize = 1, initialValue = 1)
public BigDecimal getId() {
return this.Id;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "work", cascade = CascadeType.ALL, orphanRemoval=true)
public Set<ExternalReference> getExternalReferences() {
return this.externalReferences;
}
}
This workaround is a manual way, not elegant, but works. Still hoping to find the elegant approach, it must exist somewhere, as this is a quite common use case.
I am trying to understand and figure out the solution for the following use case
These are my entity classes
User
#Entity
#Table(name = "USER")
public class User {
private UserID id;
private Set<UserAddress> addresses = new HashSet<UserAddress>(0);
#EmbeddedId
#AttributeOverrides( {
#AttributeOverride(name = "userId", column = #Column(name = "USER_ID", nullable = false, length = 32)),
#AttributeOverride(name = "userType", column = #Column(name = "USER_TYPE", nullable = false, precision = 12, scale = 0)) })
public User getId() {
return this.id;
}
#OneToMany(fetch = FetchType.EAGER, mappedBy = "user", cascade={CascadeType.ALL})
#BatchSize(size=50)
public Set<UserAddress> getAddresses() {
return this.addresses;
}
........
}
UserAddress
#Entity
#Table(name = "USERADDRESS")
public class UserAddress {
private UserID id;
Private User user;
private String address;
#EmbeddedId
#AttributeOverrides( {
#AttributeOverride(name = "userId", column = #Column(name = "USER_ID", nullable = false, length = 32)),
#AttributeOverride(name = "userType", column = #Column(name = "USER_TYPE", nullable = false, precision = 12, scale = 0)) })
public User getId() {
return this.id;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumns( {
#JoinColumn(name = "userId", referencedColumnName = "USER_ID", nullable = false, insertable=false, updatable=false),
#JoinColumn(name = "userType", referencedColumnName = "USER_TYPE", nullable = false, insertable=false, updatable=false) })
public User getUser() {
return this.user;
}
........
}
UserId
#Embeddable
public class UserId implements Serializable {
private String userNo;
private Long UserType;
.......
.......
}
I have created a staticmetamodel class for User, UserID and UserAddress and created query based on Specifications.
Metamodel class for User
#StaticMetamodel(User.class)
public abstract class User_ {
public static volatile SetAttribute<User, UserAddress> addresses;
public static volatile SingularAttribute<User, UserID> id;
}
Metamodel for UserId
#StaticMetamodel(UserID.class)
public abstract class UserID_ {
public static volatile SingularAttribute<UserID, String> userNo;
public static volatile SingularAttribute<UserID, Long> UserType;
}
I am trying to retrieve maximum of 10 User objects ordered by UserType and searched based on userId. The query has to retrieve the UserAddresses as an eager fetch.
My Specification Object is
UserSpecification
public class UserSpecifications {
public static Specification<User> userNoIs(String userNo) {
return (root, query, cb) -> {
root.fetch(User_.addresses);
return cb.equal(root.get(User_.id).get(UserID_.userNo),userNo);
};
}
}
DAO Function:
Sort sortInstructions = new Sort(Sort.Direction.DESC, "id.userNo");
Pageable pageInfo = new PageRequest(0, maxCount, sortInstructions);
Specifications<User> specifications = Specifications.where(userNoIs(input.getUserNo()));
Page<User> responseList= userRepository.findAll(specifications,pageInfo);
I am getting the following exception when I run the above statement.
Caused by: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=generatedAlias1,role=com.entity.User.addresses,tableName=USERADDRESS ,tableAlias=useraddress1_,origin=USER user0,columns={user0.USER_TYPE user0.USER_ID ,className=com.entity.UserAddress}}]
But apart from that I also need to understand how to limit the no of rows returned using Specification but without using Pageable . If I use Pageable, then a separate query will be fired to retrieve the count of rows and then actual query is being fired. My application is performance oriented, and I do not want to have any extra queries being fired.
Is there any way where I can limit the no of rows without using Pageable, but using Specifications ?