Using embedded value objects in spring data jpa projections - java

I'm working with Spring Boot 2.0RC2 and in the documentation I read you can return a projection of an entity instead of the entity as a whole when calling the Repository. This is working fine in case I use a String in my Entity but not when I use an embedded value objects.
Let's say I have the Product entity:
#Entity
#Table(name = "t_product")
public class Product extends BaseEntity {
#Column(nullable = false)
private String name;
private Product() {}
private Product(final String name) {
this.name = name;
}
public static Result<Product> create(#NonNull final String name) {
return Result.ok(new Product(name));
}
public String getName() {
return name;
}
public void setName(#NonNull final String name) {
this.name = name;
}
}
The BaseEntity simply holds the id, created and updated attributes.
I have my projection interface called ProductSummary:
interface ProductSummary {
String getName();
Long getNameLength();
}
And in my ProductRepository I have the following method that returns the ProductSummary:
public interface ProductRepository extends JpaRepository<Product, Long> {
#Query(value = "SELECT p.name as name, LENGTH(p.name) as nameLength FROM Product p WHERE p.id = :id")
ProductSummary findSummaryById(#Param("id") Long id);
}
This works perfectly fine. Now let's say I am doing DDD and instead of using a String to represent the name attribute in the Product entity, I want to use a value object called Name:
#Embeddable
public class Name implements Serializable {
public static final int MAX_NAME_LENGTH = 100;
#Column(nullable = false, length = Name.MAX_NAME_LENGTH)
private String value;
private Name() {}
private Name(final String value) {
this.value = value;
}
public static Result<Name> create(#NonNull final String name) {
if (name.isEmpty()) {
return Result.fail("Name cannot be empty");
}
if (name.length() > MAX_NAME_LENGTH) {
return Result.fail("Name cannot be longer than " + MAX_NAME_LENGTH + " characters");
}
return Result.ok(new Name(name));
}
public String getValue() {
return value;
}
}
I change my Product entity to:
#Entity
#Table(name = "t_product")
public class Product extends BaseEntity {
#Embedded
private Name name;
private Product() {}
private Product(final Name name) {
this.name = name;
}
public static Result<Product> create(#NonNull final Name name) {
return Result.ok(new Product(name));
}
public Name getName() {
return name;
}
public void setName(final Name name) {
this.name = name;
}
}
And in the ProductSummary I change the return type from String to Name.
When I run that I always get the exception:
Caused by: java.lang.IllegalAccessError: tried to access class com.acme.core.product.ProductSummary from class com.sun.proxy.$Proxy112
Can I make this work or am I missing some restriction which doesn't allow this?

If you wish to get the complete Name field(not a particular field in Name class), then you need to create another interface like ProductSummary.
interface ProductSummary {
NameSummary getName();
interface NameSummary {
String getValue();
}
}
No need to change anything in your repository.
It is quite clearly documented here
And make sure your interfaces and the methods are public.

Related

How to specify a default assembler in seedstack?

I'm using the 19.11 version of seedstack, and I want to use the FluentAssembler assembler to convert an aggregate List into a DTO List.
I'm getting the following error when I call the fluentAssembler.assemble method :
org.seedstack.business.internal.BusinessException: [BUSINESS] Unable to find assembler
Description
-----------
No assembler was found to assemble 'com.inetpsa.svr.domain.model.customer.Customer(Customer.java:1)' to
'com.inetpsa.svr.interfaces.rest.customer.CustomerRepresentation(CustomerRepresentation.java:1)'.
Fix
---
Make sure that an assembler without qualifier exists. If you want to use a qualified assembler (like a default
assembler), specify its qualifier.
I don't know howto specify the qualifier, I'd like to use a default model mapper...
Here is The Resource code :
#Path("customers")
public class CustomerResource {
#Inject
private FluentAssembler fluentAssembler;
#GET
#Produces(MediaType.APPLICATION_JSON)
public List<CustomerRepresentation> listAllCustomers() {
List<Customer> customerList = fetchAllCustomers();
return fluentAssembler.assemble(customerList).toListOf(CustomerRepresentation.class);
}
/**
* Test method - Should be replaced by a repository
* #return List<Customer> all customers
*/
private List<Customer> fetchAllCustomers(){
List<Customer> customerList = new ArrayList<>();
customerList.add(buildCustomer("005","Edward Teach","edward.teach#pirates.org"));
customerList.add(buildCustomer("006","Olivier Levasseur","olivier.levasseur#pirates.org"));
customerList.add(buildCustomer("007","James Bond","james.bond#mi6.uk"));
return customerList;
}
private Customer buildCustomer(String id, String name, String mail){
Customer result = new Customer(id);
result.updateNameAndMail(name, mail);
return result;
}
}
The aggregate :
public class Customer extends BaseAggregateRoot<String> {
#Identity
private String identifier;
private String name;
private String mail;
public Customer(String identifier){
this.identifier=identifier;
}
public void updateNameAndMail(String name, String mail){
if(StringUtils.isBlank(name)){
throw new IllegalArgumentException("Name can't be blank");
}
if(StringUtils.isBlank(mail)){
throw new IllegalArgumentException("Mail can't be blank");
}
this.name=name;
this.mail=mail;
}
public String getIdentifier() {
return identifier;
}
public String getName() {
return name;
}
public String getMail() {
return mail;
}
}
And the DTO :
#DtoOf(Customer.class)
public class CustomerRepresentation {
private String identifier;
private String name;
private String mail;
/**
* Required public no parameters constructor
*/
public CustomerRepresentation(){}
public CustomerRepresentation(String identifier, String name, String mail){
}
#AggregateId
public String getIdentifier() {
return identifier;
}
public String getMail() {
return mail;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public void setMail(String mail) {
this.mail = mail;
}
}
FluentAssembler only takes care of matching a Dto with an Assembler, but does not provide a default implementation of an Assembler by itself.
You have 2 Options to provide a Default Assembler.
Build a class that implements Asselmber
Include an addon that provides that Default Assember for you (As stated on the docs)

Room : Type of the parameter must be a class annotated with #Entity or a collection/array of it

At first, i know this is duplicate question but marked answer didn't solve the error.
I'm trying to create generic Dao interface within my Room Persistance Library following this guide but cannot solve error "Type of the parameter must be a class annotated with #Entity or a collection/array of it" for the generic T in GenericDao.
GenericDao:
#Dao
public interface GenericDao<T> {
#Insert
long insert(T object);
#Update
void update(T... objects);
#Delete
void delete(T... objects);
}
Inherited Dao:
#Dao
public abstract class ExerciseDao implements GenericDao<Exercise> {
#Query("SELECT * FROM exercises")
public abstract LiveData<List<Exercise>> getAllExercises();
}
Entity:
#Entity(tableName = "exercises",
indices = {#Index(value = {"name"}, unique = true)})
public class Exercise {
#PrimaryKey (autoGenerate = true)
int id;
#ColumnInfo(name = "name")
private String name;
#ColumnInfo(name = "muscle_part")
private int musclePart;
#ColumnInfo(name = "is_shown")
private boolean isShown;
public Exercise(String name, int musclePart, boolean isShown) {
this.name = name;
this.musclePart = musclePart;
this.isShown = isShown;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getMusclePart() {
return musclePart;
}
public void setMusclePart(int musclePart) {
this.musclePart = musclePart;
}
public boolean isShown() {
return isShown;
}
public void setShown(boolean shown) {
isShown = shown;
}
}
I've tried:
Change GenericDao to abstract class.
Add/remove #Dao annotation in GenericDao
Add Generic Entity
Override methods in child Daos
...
But always with the error.
Has anyone an idea how to solve this problem?

Map table without bean

I'm not sure if the title is right... But here is my problem
This is my bean class
#Entity
#Table(name = "Exercise")
public class Exercise {
private IntegerProperty exerciseID;
private ObjectProperty<String> name;
private ObjectProperty<ExerciseCategory> category;
private ObservableList<Parameter> parameters;
public Exercise(int exerciseID, String name, ExerciseCategory category){
this.exerciseID = new SimpleIntegerProperty(exerciseID);
this.name = new SimpleObjectProperty<>(name);
this.category = new SimpleObjectProperty<>(category);
parameters = FXCollections.observableArrayList();
}
public Exercise(){
this(0,null, null);
}
public Exercise(String name, ExerciseCategory category){
this(0, name, category);
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public int getExerciseID() {
return exerciseID.get();
}
public IntegerProperty exerciseIDProperty() {
return exerciseID;
}
public void setExerciseID(int exerciseID) {
this.exerciseID.set(exerciseID);
}
#Column(name = "name", nullable = false)
public String getName() {
return name.get();
}
public ObjectProperty<String> nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
#OneToOne
public ExerciseCategory getCategory() {
return category.get();
}
public ObjectProperty<ExerciseCategory> categoryProperty() {
return category;
}
public void setCategory(ExerciseCategory category) {
this.category.set(category);
}
public ObservableList<Parameter> getParameters() {
return parameters;
}
public void setParameters(ObservableList<Parameter> parameters) {
this.parameters = parameters;
}
}
One exercise can have one more parameters. This mapping is saved in the ExerciseParameter table, which looks like this:
ExerciseParameter
ExerciseID int(11) PK
ParameterID int(11) PK
My question is, how do I map this in the Exercise class? Because I don't want to make a ExerciseParamter class...
Thank you!
It is a many-to-many relationship. You can use #ManyToMany annotion to do that.
Add following Annotation into getParameters method of Employee Entity
#ManyToMany(cascade = {CascadeType.ALL})
#JoinTable(name="ExerciseParameter",
joinColumns={#JoinColumn(name="ExerciseID ")},
inverseJoinColumns={#JoinColumn(name="ParameterID")})
And also add following Annotation into getEmployees method of Parameter entity. (You have not presented the Parameter Entity. I assume that Parameter class contains the getEmployees method.)
#ManyToMany(mappedBy="parameters")

Neo4j OGM RelationshipEntity when StartNode and EndNode are objects of same type

I am following the Neo4j OGM guide at - http://neo4j.com/docs/ogm/java/stable/
For Relationship entities, we need to have a start node and an end node. I've modified the example a little bit (to make it simpler like this) -
#NodeEntity
public class Student extends Entity {
private String name;
#Relationship(type= "ENROLLED")
private Enrollment enrollment = new Enrollment();
public String getName() {
return name;
}
public Enrollment getEnrollment() {
return enrollment;
}
public void setEnrollment(Enrollment enrollment) {
this.enrollment = enrollment;
}
public Student() {
}
public Student(String name) {
this.name = name;
}
}
#NodeEntity
public class Course extends Entity {
private String name;
public String getName() {
return name;
}
#Relationship(type= "ENROLLED", direction= Relationship.INCOMING)
private Enrollment enrollment = new Enrollment();
public Enrollment getEnrollment() {
return enrollment;
}
public void setEnrollment(Enrollment enrollment) {
this.enrollment = enrollment;
}
public Course() {
}
public Course(String name) {
this.name = name;
}
}
#RelationshipEntity(type = "ENROLLED")
public class Enrollment extends Entity {
#StartNode
private Student student;
#EndNode
private Course course;
private Date enrolledDate;
public Student getStudent() {
return student;
}
public Course getCourse() {
return course;
}
public Date getEnrolledDate() {
return enrolledDate;
}
public Enrollment() {
}
public Enrollment(Student student, Course course, Date enrolledDate) {
this.student = student;
this.course = course;
this.enrolledDate = enrolledDate;
}
}
Now when I try to save this in Neo4j, it works fine. However in my scenario, the type of StartNode and EndNode objects are the same -
#NodeEntity
public class MyObject extends Entity {
private String name;
#Relationship(type="CONNECTION")
private MyConnection startConnection = new MyConnection();
#Relationship(type="CONNECTION", direction= Relationship.INCOMING)
private MyConnection endConnection = new MyConnection();
public String getName() {
return name;
}
public MyConnection getStartConnection() {
return startConnection;
}
public void setStartConnection(MyConnection myConnection) {
this.startConnection = myConnection;
}
public MyConnection getEndConnection() {
return endConnection;
}
public void setEndConnection(MyConnection endConnection) {
this.endConnection = endConnection;
}
public MyObject() {
super();
}
public MyObject(String name) {
super();
this.name = name;
}
}
#RelationshipEntity(type="CONNECTION")
public class MyConnection extends Entity {
#StartNode
private MyObject start;
#EndNode
private MyObject end;
private String name;
public String getName() {
return name;
}
public MyConnection() {
super();
}
public MyConnection(MyObject start, MyObject end, String name) {
super();
this.start = start;
this.end = end;
this.name = name;
}
}
When I try to save these using -
public class Main {
public static void main(String[] args) {
Session session = Neo4jSessionFactory.getInstance().getNeo4jSession();
Student s = new Student("manoj");
Course c = new Course("physics");
Enrollment e = new Enrollment(s, c, new Date());
s.setEnrollment(e);
c.setEnrollment(e);
MyObject startObj = new MyObject("Start Object");
MyObject endObj = new MyObject("End Object");
MyConnection conn = new MyConnection(startObj, endObj, "Connection");
startObj.setStartConnection(conn);
endObj.setEndConnection(conn);
try(Transaction tx = session.beginTransaction()) {
session.save(s);
session.save(c);
session.save(e);
session.save(startObj);
session.save(endObj);
session.save(conn);
tx.commit();
}
}
}
The student, course and enrollments objects get saved, but the two MyObject and the MyConnection objects don't get saved and I get the following exception -
Exception in thread "main" java.lang.NullPointerException
at org.neo4j.ogm.metadata.MetaData.classInfo(MetaData.java:76)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapRelationshipEntity(EntityGraphMapper.java:389)
at org.neo4j.ogm.mapper.EntityGraphMapper.link(EntityGraphMapper.java:319)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapEntityReferences(EntityGraphMapper.java:262)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapEntity(EntityGraphMapper.java:154)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapRelatedEntity(EntityGraphMapper.java:528)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapRelationshipEntity(EntityGraphMapper.java:420)
at org.neo4j.ogm.mapper.EntityGraphMapper.link(EntityGraphMapper.java:319)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapEntityReferences(EntityGraphMapper.java:262)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapEntity(EntityGraphMapper.java:154)
at org.neo4j.ogm.mapper.EntityGraphMapper.map(EntityGraphMapper.java:87)
at org.neo4j.ogm.session.delegates.SaveDelegate.save(SaveDelegate.java:65)
at org.neo4j.ogm.session.delegates.SaveDelegate.save(SaveDelegate.java:41)
at org.neo4j.ogm.session.Neo4jSession.save(Neo4jSession.java:370)
at neo4j.ogm.ex.Main.main(Main.java:37)
Can you please help me to solve this -
1) Is it necessary that the StartNode and EndNode objects be of different types?
2) Is there some problem with my code or is it a shortcoming of the Neo4j OGM?
Thanks in advance,
Manoj.
Update after trying Luanne's suggestion -
Thanks Luanne. I tried your suggestion, though I had to specify the URL differently. I used -
http://m2.neo4j.org/content/repositories/snapshots
because by default it used https and I was getting some security exception and this dependency was not getting downloaded.
Anyways, with the 1.1.1-SNAPSHOT version, I still get the below error -
Exception in thread "main" java.lang.NullPointerException
at org.neo4j.ogm.metadata.MetaData.classInfo(MetaData.java:80)
at org.neo4j.ogm.mapper.EntityGraphMapper.haveRelationEndsChanged(EntityGraphMapper.java:391)
at org.neo4j.ogm.mapper.EntityGraphMapper.getRelationshipBuilder(EntityGraphMapper.java:362)
at org.neo4j.ogm.mapper.EntityGraphMapper.link(EntityGraphMapper.java:325)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapEntityReferences(EntityGraphMapper.java:276)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapEntity(EntityGraphMapper.java:157)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapRelatedEntity(EntityGraphMapper.java:571)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapRelationshipEntity(EntityGraphMapper.java:473)
at org.neo4j.ogm.mapper.EntityGraphMapper.link(EntityGraphMapper.java:329)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapEntityReferences(EntityGraphMapper.java:276)
at org.neo4j.ogm.mapper.EntityGraphMapper.mapEntity(EntityGraphMapper.java:157)
at org.neo4j.ogm.mapper.EntityGraphMapper.map(EntityGraphMapper.java:90)
at org.neo4j.ogm.session.delegates.SaveDelegate.save(SaveDelegate.java:67)
at org.neo4j.ogm.session.delegates.SaveDelegate.save(SaveDelegate.java:43)
at org.neo4j.ogm.session.Neo4jSession.save(Neo4jSession.java:376)
at neo4j.ogm.ex.Main.main(Main.java:37)
Is your intent to model MyObject to contain a single outgoing relationship type CONNECTION and a single incoming relationship type CONNECTION where each relationship has a property name?
So if we're looking at EndObject, then startConnection is the relationship with name conn2 and endConnection is the relationship with name conn1?
If so, we might have a problem here. https://jira.spring.io/browse/DATAGRAPH-728
Update:
This wasn't a bug after all. The problem is the initialization you have in MyObject:
#Relationship(type="CONNECTION")
private MyConnection startConnection = new MyConnection();
#Relationship(type="CONNECTION", direction= Relationship.INCOMING)
private MyConnection endConnection = new MyConnection();
You've initialized both startConnection and endConnection to an invalid relationship entity i.e. one with no start and no end node.
In your test, you set the startConnection on startObj but not the end. In effect, the endConnection is represented by the invalid relationship entity.
Remove the initialization and it should work as you expect.
Thanks Luanne for your answer / suggestion. I came across your excellent article The essence of Spring Data Neo4j 4 and realized that my need is very similar to the Ingredient-Pairing example (mentioned in your article). For me, there can be a connection (MyConnection) between two objects (MyObject). So my modified code looks like this -
package neo4j.ogm.ex.domain;
import java.util.HashSet;
import java.util.Set;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;
#NodeEntity
public class MyObject extends Entity {
private String name;
#Relationship(type="CONNECTION", direction= Relationship.UNDIRECTED)
private Set<MyConnection> connections = new HashSet<>();
public String getName() {
return name;
}
public Set<MyConnection> getConnections() {
return connections;
}
public void addConnection(MyConnection myConnection) {
myConnection.getStart().getConnections().add(myConnection);
myConnection.getEnd().getConnections().add(myConnection);
}
public MyObject() {
super();
}
public MyObject(String name) {
super();
this.name = name;
}
}
package neo4j.ogm.ex.domain;
import org.neo4j.ogm.annotation.EndNode;
import org.neo4j.ogm.annotation.RelationshipEntity;
import org.neo4j.ogm.annotation.StartNode;
#RelationshipEntity(type="CONNECTION")
public class MyConnection extends Entity {
#StartNode
private MyObject start;
#EndNode
private MyObject end;
private String name;
public String getName() {
return name;
}
public MyObject getStart() {
return start;
}
public MyObject getEnd() {
return end;
}
public MyConnection() {
super();
}
public MyConnection(MyObject start, MyObject end, String name) {
super();
this.start = start;
this.end = end;
this.name = name;
}
}
And it works perfectly. I'll also try out your other suggestion of removing the invalid initialization.
Thanks again,
Manoj.

Create Neo4j relation in spring data with existing node

I am trying to create relationships between nodes in Neo4j. I am using the Neo4J(2.1.8 Community) & spring-data-neo4j(3.3.0.RELEASE).
I am trying to create the below relation.
Create a new Employee(node) which will be reporting(Empty Relation) to the Manager(node) which is there in DB (Searching by name). I have used the below query.
public interface EmployeeRepository extends GraphRepository<Employee> {
#Query("START employee=node:({0}), manager=node:Employee(name={1}) CREATE employee-[:REPORTS_TO]->manager")
void addNewEmployee(Employee employee, String managerName);}
I got the below error.
Caused by: org.springframework.dao.InvalidDataAccessResourceUsageException: Error executing statement START employee=node:({0}), manager=node:Employee(name={1}) CREATE employee-[:REPORTS_TO]->manager; nested exception is Invalid input '(': expected whitespace or an identifier (line 1, column 21)
"START employee=node:({0}), manager=node:Employee(name={1}) CREATE employee-[:REPORTS_TO]->manager"
^
Can anyone please tell me what is wrong with this query? Also if this is not the right way to create the relations using GraphRepository then what else I can use to accomplish the same.
Thanks in advance.
Note: I have used this to learn the queries in Spring Data for Neo4j. Where they have shown the basic queries.
Updated: Employee Class
#NodeEntity
public class Employee {
#GraphId
private Long id;
private String name;
private String department;
#RelatedTo(type = "REPORTS_TO")
private Employee reportsTo;
#RelatedTo(type = "REPORTS_TO", direction = Direction.INCOMING)
Set<Employee> directReport;
public Employee() {
}
public Employee(String name, String department) {
this.name = name;
this.department = department;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public Employee getReportsTo() {
return reportsTo;
}
public void setReportsTo(Employee reportsTo) {
this.reportsTo = reportsTo;
}
public Set<Employee> getDirectReport() {
return directReport;
}
public void setDirectReport(Set<Employee> directReport) {
this.directReport = directReport;
}
#Override
public int hashCode() {
return super.hashCode();
}
#Override
public boolean equals(Object obj) {
return super.equals(obj);
}
#Override
public String toString() {
return "Employee{" + "id=" + id + ", name=" + name + ", department=" + department + '}';
}
}
Please show the definition of employee!
You can also use template.createRelationshipBetween(nodeOrEntity,nodeOrEntity2,type)
For your example this should work:
public interface EmployeeRepository extends GraphRepository {
#Query("MATCH (employee:Employee), (manager:Employee)
WHERE id(employee) = {0} AND manager.name = {1}
CREATE employee-[:REPORTS_TO]->manager")
void addNewEmployee(Employee employee, String managerName);
}
perhaps better use, employee as a data-container / map for employee or just pass in the name directly:
#Query("MATCH (employee:Employee), (manager:Employee)
WHERE employee.name = {0}.name AND manager.name = {1}
CREATE employee-[:REPORTS_TO]->manager")
void addNewEmployee(Employee employee, String managerName);
}

Categories

Resources