spring-data-jdbc: query entity containing 1-n relation with JOOQ - spring-boot

I am trying to load entities containing a reference to another entity (1-n) with the help of JOOQ (based on spring-data-jdbc).
I'm started extending the spring-data-jdbc-jooq-example.
The adjusted model with the 1-n relation:
#Data
public class Category {
private #Id Long id;
private String name, description;
private AgeGroup ageGroup;
private Set<SubCategory> subCategories;
public Category() {}
public Category(Long id, String name, String description, AgeGroup ageGroup) {
this(id, name, description, ageGroup, new HashSet<>());
}
public Category(Long id, String name, String description, AgeGroup ageGroup, Set<SubCategory> subCategories) {
this.id = id;
this.name = name;
this.description = description;
this.ageGroup = ageGroup;
this.subCategories = subCategories;
}
}
#Data
#AllArgsConstructor
#NoArgsConstructor
public class SubCategory {
private #Id Long id;
private String title;
}
I wrote two queries, one via the #Query-Annotation in the CrudRepository and one with the help of JOOQ in the JooqRepository.
interface CategoryRepository extends CrudRepository<Category, Long>, JooqRepository {
#Query("SELECT * FROM category")
List<Category> findAllWithQuery();
}
public interface JooqRepository {
List<Category> findAllWithJooq();
}
public class JooqRepositoryImpl implements JooqRepository {
private final DSLContext dslContext;
public JooqRepositoryImpl(DSLContext dslContext) {
this.dslContext = dslContext;
}
#Override
public List<Category> findAllWithJooq() {
return dslContext.select()
.from(CATEGORY)
.fetchInto(Category.class);
}
}
(for me both methods should return the same result-set b/c they execute the same query?!)
But my unit-test fails:
#Test
public void exerciseRepositoryForSimpleEntity() {
// create some categories
SubCategory sub0 = new SubCategory(null, "sub0");
SubCategory sub1 = new SubCategory(null, "sub1");
Category cars = new Category(null, "Cars", "Anything that has approximately 4 wheels", AgeGroup._3to8, Sets.newLinkedHashSet(sub0, sub1));
// save category
repository.saveAll(asList(cars));
// execute
List<Category> actual = repository.findAllWithJooq();
List<Category> compare = repository.findAllWithQuery();
Output.list(actual, "JOOQ");
Output.list(compare, "Query");
// verify
assertThat(actual).as("same size of categories").hasSize(compare.size());
assertThat(actual.get(0).getSubCategories()).as("same size of sub-categories").hasSize(compare.get(0).getSubCategories().size());
}
with
java.lang.AssertionError: [same size of sub-categories]
Expecting actual not to be null
As you can see in the following output the sub-categories queried by JOOQ will not be loaded:
2019-11-26 16:28:00.749 INFO 18882 --- [ main] example.springdata.jdbc.jooq.Output : ==== JOOQ ====
Category(id=1,
name=Cars,
description=Anything that has approximately 4 wheels,
ageGroup=_3to8,
subCategories=null)
2019-11-26 16:28:00.749 INFO 18882 --- [ main] example.springdata.jdbc.jooq.Output : ==== Query ====
Category(id=1,
name=Cars,
description=Anything that has approximately 4 wheels,
ageGroup=_3to8,
subCategories=[SubCategory(id=1,
title=sub0),
SubCategory(id=2,
title=sub1)])
This is the used database-shema:
CREATE TABLE IF NOT EXISTS category (
id INTEGER IDENTITY PRIMARY KEY,
name VARCHAR(100),
description VARCHAR(2000),
age_group VARCHAR(20)
);
CREATE TABLE IF NOT EXISTS sub_category (
id INTEGER IDENTITY PRIMARY KEY,
title VARCHAR(100),
category INTEGER
)

In the JOOQ variant, JOOQ does the conversion from ResultSet to object instances. Since JOOQ doesn't know about the interpretation of aggregates as it is done by Spring Data JDBC it only hydrates the Category itself, not the contained Set of SubCategory.
Spring Data JDBC on the other hand interprets the structure of the Category and based on that executes another statement to load the subcategories.

Related

Spring JPA CriteriaQuery groupBy based on only one key in a Composite Primary Key (#EmbeddedId)

I am trying to write a criteriaQuery and group results based on emailId, which is one of the keys of a composite PK, embedded into one of my Entity classes.
The method that returns the specification is as follows :
public static Specification < User > getSpecification(Integer id) {
return (root, query, criteriaBuilder) - > {
var predicates = new ArrayList<Predicate()>;
predicates.add(criteriaBuilder.equal(root.get("indexId"), id));
query.groupBy(root.get("details").get("emailId"));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
The entity class:
public class User {
#EmbeddedId private Details details;
private String name;
private String status;
}
#Embeddable
public class Details {
private String emailId;
private Integer branchId;
}
I have skipped some annotations.
I want to group the results in a way where emailId remains unique, even if branchId changes. In essence for data where there are 3 rows of same emailId but different branchId, I should only fetch 1 result.
It seems it throws an error and asks for both components of the composite key to be passed in the query.groupBy statement.
If you could please help me figure out the issue.

Updating entity with One to One relationship using Spring Data

I have an issue when updating entity with OneToOne relationship, it creates record instead of updating the existing one. Below are the sample entities.
#Entity
#Table(schema = "crm", name = "persons")
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
#OneToOne(mappedBy = "person", fetch = FetchType.LAZY)
public Employee getEmployee() {
return employee;
}
}
#Entity
#Table(schema = "crm", name = "employees")
public class Employee {
#Id
public Long getId() {
return id;
}
#OneToOne
#MapsId //Use the person PK id value as Employee PK id
#JoinColumn(name = "id")
public Person getPerson() {
return person;
}
}
I am using the PagingAndSortingRepository of Spring Data. Below is the service layer to update the entity.
#Override
#Transactional
public EmployeeResponse updateEmployee(Employee aEmployee) {
EmployeeResponse response = new EmployeeResponse();
try {
Optional<Employee> probableEmployee = employeeRepository.findById(aEmployee.getId());
if (!probableEmployee.isPresent()) {
throw new RecordNotFoundException(String.format(MessageConstants.EMPLOYEE_ID_NOT_FOUND, aEmployee.getId()));
}
Employee existingEmployeeToUpdate = probableEmployee.get();
EmployeeEntityHelper.updateExistingEntity(aEmployee, existingEmployeeToUpdate);
existingEmployeeToUpdate = employeeRepository.save(existingEmployeeToUpdate);
response.setSuccessfulResponse(existingEmployeeToUpdate);
} catch (Exception ex) {
LOGGER.error(ex.getLocalizedMessage(), ex);
response.setErrorAttributes(false, ReturnCode.FAILED.getCode(), ex.getLocalizedMessage());
}
return response;
}
The EmployeeEntityHelper.updateExistingEntity(source, target) will simply copy all properties of entities from source to target.
The save() method will generate an insert for Person even if I am explicitly passing the id existing in DB. But for employee it will generate an update which is expected.
Below is the updateExistingEntity() method:
public static void updateExistingEntity(Employee source, Employee target) {
copyProperties(source, target, Arrays,asList("person", "employeeNumber", "hiredDate", "birthDate"));
}
private static void copyProperties(Object aSource, Object aTarget, Iterable<String> aProperties) {
BeanWrapper sourceWrapper = PropertyAccessorFactory.forBeanPropertyAccess(aSource);
BeanWrapper targetWrapper = PropertyAccessorFactory.forBeanPropertyAccess(aTarget);
aProperties.forEach(p ->
targetWrapper.setPropertyValue(p, sourceWrapper.getPropertyValue(p))
);
}
Generally, a new INSERT instead of UPDATEs could be due to:
Either entity passed to save has no ID set, thus persist is called under the hood
Or entity with given ID is not present in the database thus merge fails (and maybe it is persisted as a fallback, dunno)
Check if entities have set ID field.
In your model , override equal/hash-code and just chek the id parameter
maybe it will help you :)
The issue is already fixed, somehow the copyproperties logic is not right in the sense that I am updating/setting the person also.

Using Spring Data JPA - how to save ONLY UNIQUE items in child entity while saving parent entity and using CascadeType=PERSIST

I am preparing simple Spring app. I have 2 entities :
Book.class (parent) and Author.class (child): with #OneToMany from Author view and #ManyToOne(cascade=CascadeType.PERSIST) from Book view relations. While saving new Book - also Author is being saved and added to DB( mySql)- which is what I want. But I cannot understand why Spring adds Author - if such item already exists. How to change the code to make sure that only unique Authors will be added to DB and there will be no duplicates in author table in DB?
I've added hashCode and equals methods to Author class but it did not help.
I've tried to change also Cascade.Type but also did not help.
The Author.class(part of code):
#Entity
public class Author {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
#OneToMany(mappedBy = "author")
#JsonManagedReference(value = "book-author")
private Set<Book> books = new HashSet<Book>();
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Author author = (Author) o;
return Objects.equals(getFirstName(), author.getFirstName()) &&
Objects.equals(getLastName(), author.getLastName());
}
#Override
public int hashCode() {
return Objects.hash(getFirstName(), getLastName());
}
And the Book.class(part of code):
#Entity
public class Book {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
private String title;
#ManyToOne( cascade=CascadeType.PERSIST)
#JoinColumn(name = "author_id", unique = true)
#JsonBackReference(value="book-author")
private Author author;
Edit 1
BookServiceImpl.class
#Override
public BookDto addBookDto(BookDto bookDto) {
Book book = bookConverter.apply(bookDto);
bookRepository.save(book);
return bookDtoConverter.apply(book);
}
AuthorServiceImpl.class
#Override
public Author findAuthor(String firstName, String lastName) {
Optional<Author> authorByNameOptional = authorRepository.findByFirstNameAndLastName(firstName, lastName);
if (authorByNameOptional.isPresent()) {
return authorByNameOptional.get();
} else {
Author newAuthor = new Author();
newAuthor.setFirstName(firstName);
newAuthor.setLastName(lastName);
return newAuthor;
}
And BookWebController.class
#PostMapping("/addWebBook")
public String addBook(#ModelAttribute(name = "addedBook") BookDto addedBook, Model model) {
Author author1 = addedBook.getAuthor();
Author author = authorService.findAuthor(author1.getFirstName(), author1.getLastName());
addedBook.setAuthor(author);
bookService.addBookDto(addedBook);
return "redirect:/message?msg";
}
Would be greatful for any hint as I am quite new to this area :-)
Let me suggest that your Author object in Book has empty primary key field? Hibernate's logic in this case: empty id means new row to insert. It may work correctly if you set a primary key to author. For example, user can find author (with it's PK) or add new (without PK) and call save(book) method which will cascadely persists only new author. In most cases that usually works like that.
Another moment to pay attention, that if you wanna keep author's uniqueness in database, than you must to put constraint on the author's entity.
For example if each author must have unique first name and last name it may look something like this:
#Entity
#Table(uniqueConstraints= #UniqueConstraint(columnNames={"first_name", "last_name"}))
public class Author {
...
After that, DataIntegrityViolationException will be thrown on duplicating value insertion and your database will stay clean of duplicates.

Relationship Exists in neo4j but not in Spring #NodeEntity

I have a class in my domain called Activity that looks like the following
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
#NodeEntity
public class Activity {
#GraphId
private Long id;
private String title;
private String description;
#Relationship(type = "RELATED_TO", direction = Relationship.UNDIRECTED)
private List<Activity> relatedActivities = new ArrayList<>();
public Activity() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public Collection<Activity> getRelatedActivities() {
System.out.println("getting relatedActivities");
System.out.println(relatedActivities);
return relatedActivities;
}
public void addRelatedActivity(Activity activity) {
this.relatedActivities.add(activity);
}
}
I create relationships using the following repository class:
#RepositoryRestResource(collectionResourceRel = "relationships", path = "relationships")
public interface RelationshipRepository extends GraphRepository<Relationship> {
#Query("MATCH (a1:Activity), (a2:Activity) " +
"WHERE a1.title = {0} AND a2.title = {1}" +
"CREATE (a1)-[:RELATED_TO]->(a2)")
void addRelationship(String a1Title, String a2Title);
}
I have verified that this code works using the neo4j browser, which lets me see existing nodes and relationships between them. However, when I access getRelatedActivities() on an Activity object, it's always an empty array, even if that Activity has other Activity nodes related to it, clearly visible in neo4j.
How can I get the relatedActivites on an Activity to automatically populate based on its relationships correctly?
The problem in your code is that you define the "target" as an Activity here
#Relationship(type = "RELATED_TO", direction = Relationship.UNDIRECTED)
private List<Activity> relatedActivities = new ArrayList<>();
but you also have a RelationshipEntity class in your code base: Relationship with the same type RELATED_TO.
When OGM gets the result it tries to match every field but since it converts the relationship type RELATED_TO to the RelationshipEntity and not an Activity object, it does not fill the list in the Activity class.

Store enum name, not value in database using EBean

I have this enum :
public enum DocumentTypes {
PDF("PDF Document"), JPG("Image files (JPG)"), DOC("Microsoft Word documents");
private final String displayName;
DocumentTypes(final String display) {
this.displayName = display;
}
#Override
public String toString() {
return this.displayName;
}
}
And a model like this :
#Entity
#Table(name = "documents")
public class Document extends Model {
#Id
public Long id;
#Constraints.Required
#Formats.NonEmpty
#Enumerated(EnumType.STRING)
#Column(length=20, nullable=false)
public DocumentTypes type;
#Constraints.Required
#Formats.NonEmpty
#Column(nullable=false)
public String document;
}
I match the enum using this in my controller :
DynamicForm form = form().bindFromRequest();
// ...
Document doc = new Document();
doc.type = DocumentTypes.valueOf(form.field("type").value());
doc.save();
The problem is that in database, it's stored as "Microsoft Word documents", but I would prefer to store it as DOC.
How can I do that?
You can define it very fine granular with the Anotation EnumMapping or EnumValue. This works with the old version org.avaje.ebean.
It seems that there was a complete rewrite of the code. In the actual version there is a different approach.

Resources