Will Spring JPA repository save() do an update, sometimes? - spring

I've two JPA entities, say A and B. Entity A composes B and has a OneToOne mapping with cascade=MERGE, orphanRemoval=false, etc. The persistence execution is as below
Create B as, B b = new B() and set only pre-persistence data, some date info is automatically set when object is saved.
Create A as, A a = new A() and set all mandatory fields and also set b to a.
respotory.save(a)
Everything is good. Both A, B are saved and good.
Next, fetch A using repository (A oldA = repo.findOne(key)). Transform oldA into another similar application object, say, appA (of Type A2) and do some application logic and then create a NEW object a1 of type entity A and map all the data from oldA.
Here the object a1 has exact similar data as fetched oldA, BUT the only different is, a1 composes a NEW B() object, say b1 and b1 has the same key as 'b' but is missing some mandatory dates (which were supposed to be set while saving, as a result of jpa annotations)
Here I've a couple of queries when saving new entity.
When I save the entity a1, I'm getting an exception as b1 is missing mandatory attribute. I'm not getting how it worked first time and not now?
Also, Does hibernate compare the keys of both a1 and oldA while saving a1 and executes an update() rather than an insert().?
If it is the update operation this time, then only I can get an exception as mandatory data missing on 'b1' as one of the mandatory attribute is supposed to be set during #PrePersist, not while #PreUpdate.
Why would hibernate does an update and NOT an insert ? I created NEW objects for A and B, second time.
Thanks

Hibernate will check, if the id field is null. If so, then it will insert the data, if the id field has a value, Hibernate will do an update to the row with that id. So if you create a new object and copy values from another object, and you want to create a new entry in the database, make sure the id field stays null.

Related

Is it possible to retrieve data in resources folder instead of repository in particular case?

I have a repository which request my database, but in a particular case, I would like it to get data from a JSON (which have the same structure than my entity).
I've tried :
AOP : before each repository methods, if we are in the particular case, I get the data from a JSON and I can cast it to the correct class (because with AOP i can get the return type of the method). So it's working for methods like :
#Query("SELECT e FROM entity e WHERE e.name = :name")
Entity getAllEntities(String name);
but not with methods like ...
#Query("SELECT n.attribute FROM entity e, nested n WHERE e.name = :name")
String getNestedAttribute(String name);
... because I can't figure out where I could find the data in JSON. Except if you know how I could get the path (for exemple : $['entity']['nested'][0]['attribute']) from an SQL query so I can use this library
Repository populator
On application startup, it replace the data in the database with the value I store in JSON. The problem with it is that I don't want to loose my data ...
I know this post is weird but I don't want to add those data in my database because it would affect to much application ...

Spring Data / Hibernate save entity with Postgres using Insert on Conflict Update Some fields

I have a domain object in Spring which I am saving using JpaRepository.save method and using Sequence generator from Postgres to generate id automatically.
#SequenceGenerator(initialValue = 1, name = "device_metric_gen", sequenceName = "device_metric_seq")
public class DeviceMetric extends BaseTimeModel {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "device_metric_gen")
#Column(nullable = false, updatable = false)
private Long id;
///// extra fields
My use-case requires to do an upsert instead of normal save operation (which I am aware will update if the id is present). I want to update an existing row if a combination of three columns (assume a composite unique) is present or else create a new row.
This is something similar to this:
INSERT INTO customers (name, email)
VALUES
(
'Microsoft',
'hotline#microsoft.com'
)
ON CONFLICT (name)
DO
UPDATE
SET email = EXCLUDED.email || ';' || customers.email;
One way of achieving the same in Spring-data that I can think of is:
Write a custom save operation in the service layer that
Does a get for the three-column and if a row is present
Set the same id in current object and do a repository.save
If no row present, do a normal repository.save
Problem with the above approach is that every insert now does a select and then save which makes two database calls whereas the same can be achieved by postgres insert on conflict feature with just one db call.
Any pointers on how to implement this in Spring Data?
One way is to write a native query insert into values (all fields here). The object in question has around 25 fields so I am looking for an another better way to achieve the same.
As #JBNizet mentioned, you answered your own question by suggesting reading for the data and then updating if found and inserting otherwise. Here's how you could do it using spring data and Optional.
Define a findByField1AndField2AndField3 method on your DeviceMetricRepository.
public interface DeviceMetricRepository extends JpaRepository<DeviceMetric, UUID> {
Optional<DeviceMetric> findByField1AndField2AndField3(String field1, String field2, String field3);
}
Use the repository in a service method.
#RequiredArgsConstructor
public class DeviceMetricService {
private final DeviceMetricRepository repo;
DeviceMetric save(String email, String phoneNumber) {
DeviceMetric deviceMetric = repo.findByField1AndField2AndField3("field1", "field", "field3")
.orElse(new DeviceMetric()); // create new object in a way that makes sense for you
deviceMetric.setEmail(email);
deviceMetric.setPhoneNumber(phoneNumber);
return repo.save(deviceMetric);
}
}
A word of advice on observability:
You mentioned that this is a high throughput use case in your system. Regardless of the approach taken, consider instrumenting timers around this save. This way you can measure the initial performance against any tunings you make in an objective way. Look at this an experiment and be prepared to pivot to other solutions as needed. If you are always reading these three columns together, ensure they are indexed. With these things in place, you may find that reading to determine update/insert is acceptable.
I would recommend using a named query to fetch a row based on your candidate keys. If a row is present, update it, otherwise create a new row. Both of these operations can be done using the save method.
#NamedQuery(name="getCustomerByNameAndEmail", query="select a from Customers a where a.name = :name and a.email = :email");
You can also use the #UniqueColumns() annotation on the entity to make sure that these columns always maintain uniqueness when grouped together.
Optional<Customers> customer = customerRepo.getCustomersByNameAndEmail(name, email);
Implement the above method in your repository. All it will do it call the query and pass the name and email as parameters. Make sure to return an Optional.empty() if there is no row present.
Customers c;
if (customer.isPresent()) {
c = customer.get();
c.setEmail("newemail#gmail.com");
c.setPhone("9420420420");
customerRepo.save(c);
} else {
c = new Customer(0, "name", "email", "5451515478");
customerRepo.save(c);
}
Pass the ID as 0 and JPA will insert a new row with the ID generated according to the sequence generator.
Although I never recommend using a number as an ID, if possible use a randomly generated UUID for the primary key, it will qurantee uniqueness and avoid any unexpected behaviour that may come with sequence generators.
With spring JPA it's pretty simple to implement this with clean java code.
Using Spring Data JPA's method T getOne(ID id), you're not querying the DB itself but you are using a reference to the DB object (proxy). Therefore when updating/saving the entity you are performing a one time operation.
To be able to modify the object Spring provides the #Transactional annotation which is a method level annotation that declares that the method starts a transaction and closes it only when the method itself ends its runtime.
You'd have to:
Start a jpa transaction
get the Db reference through getOne
modify the DB reference
save it on the database
close the transaction
Not having much visibility of your actual code I'm gonna abstract it as much as possible:
#Transactional
public void saveOrUpdate(DeviceMetric metric) {
DeviceMetric deviceMetric = metricRepository.getOne(metric.getId());
//modify it
deviceMetric.setName("Hello World!");
metricRepository.save(metric);
}
The tricky part is to not think the getOne as a SELECT from the DB. The database never gets called until the 'save' method.

Converting Object to Class object

in my Spring MVC project i m using Hibernate, by using Criteria API i am applying Group BY and Order BY clause. Query get executed on DB successfully and it brings result also but its an array of Object--
Here is code of Criteria API
Criteria criteria = session.createCriteria(DashboardSubindicatorSubmission.class, "DashboardSubindicatorSubmission")
.setProjection(Projections.projectionList()
.add(Projections.sum("InputValue").as("InputValue"))
.add(Projections.groupProperty("fkAccademicYearId"))
.add(Projections.groupProperty("fkAssessmentPlanID"))
.add(Projections.groupProperty("fkSubindicatorID"))
.add(Projections.groupProperty("InputTitle")))
.addOrder(Order.asc("fkAccademicYearId"))
.addOrder(Order.asc("fkAssessmentPlanID"))
.addOrder(Order.asc("InputTitle"));
List<DashboardSubindicatorSubmission> dashboardSubindicatorSubmissionList = (List<DashboardSubindicatorSubmission>)criteria.list();
session.flush();
transaction.commit();
return dashboardSubindicatorSubmissionList;
I am casting criteria.list() to List<DashboardSubindicatorSubmission> but when i try to do dashboardSubindicatorSubmissionList.get(i) on controller it gives me exception java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to mkcl.accreditation.model.DashboardSubindicatorSubmission.
i come to know that, though i m casting it to List<DashboardSubindicatorSubmission> still its an list of object[] thats why i cant do dashboardSubindicatorSubmissionList.get(i) because it returns me object of DashboardSubindicatorSubmission. (Correct me if i am wrong)
So how can i convert my result into list of DashboardSubindicatorSubmission class?
Does setResultTransformer() helps me in this case?
You have two options. When you use projections, Hibernate doesn't know how to respect each field because it uses the name of each field to build objects and he doesn't know the names yet.
Thus, your first option is to name the fields grouped so that they match the names of object properties. This is necessary even if the string you use in projection is already the name of the object field. Something like:
.add(Projections.groupProperty("fkAccademicYearId"), "fkAccademicYearId") // same value
.add(Projections.groupProperty("fkAssessmentPlanID"), "other") // other value
The second option is to do what you yourself suggested, create your own implementation of ResultTransformer. I reckon this a interesting option if you want to extract other object of this query, as when you make a report.

Specifying a list of fields in HQL doesn't seem to work

I have the following HQL in Hibernate using Spring MVC.
List<Colour>list=session.createQuery("from Colour order by colourId desc")
.setFirstResult((currentPage-1)*rowsPerPage)
.setMaxResults(rowsPerPage).list();
It works and returns a list of rows from the colour table (actually operates upon the Colour entity (POJO) that I can understand) in Oracle 10g.
What if I need to retrieve a list fields, I'm trying the following.
List<Colour>list=session.createQuery("colourId, colourName, colourHex from Colour order by colourId desc")
.setFirstResult((currentPage-1)*rowsPerPage)
.setMaxResults(rowsPerPage).list();
It ends with an excpetion
java.lang.IllegalArgumentException: node to traverse cannot be null!
In some articles, it was mentioned that the following version of HQL should (or may) work
List<Colour>list=session.createQuery("select colourId, colourName, colourHex from Colour order by colourId desc")
.setFirstResult((currentPage-1)*rowsPerPage)
.setMaxResults(rowsPerPage).list();
but unfortunately, it also didn't work for me. Using the createSQLQuery() method to execute native SQL would work but I want to stick to the createQuery() method with HQL unless it's absolutely necessary. How can I specify a list of fields in HQL?
I agree with yorkw's comment. If you select properties in your query then you cannot ask for a List<Colour> object to be returned from a call to .list().
Instead you should do this
List<Object[]> rows = session.createQuery("select c.colourId, c.colourName, c.colourHex " +
" from Colour c " +
" order by c.colourId desc").list();
Then iterate over the list object and instantiate your objects. Or whatever you need to do.
for ( Object[] row : rows ) {
Long colourId = (Long)row[0];
// ... etc
}
Why don't you try creating a map? Something like this:
SELECT NEW MAP( colour.colourId AS id
, colour.colourName AS name ...)
FROM Colour colour
ORDER BY colour.colourId
I use the alias for Colour "colour" so hibernate knows from which entity is the property I am referencing, I am implying all those properties are from the same entity, if not, then check your referencing!

Delphi7 master detail relations query results in ORA-01036

I'm using Delphi7, Devart's dbExpress driver 4.70.
I drop two TSQLTables (call them A and B), two TDataSetProviders (dspA and dspB), two TClientDataSets (cdsA and cdsB), two TDataSources (dsA and dsB) and two DBGrids (gridA and gridB). Everything is set fine. If I set cdsA.Active to true I can see the data in gridA. The same per cdsB.
Now I want to implement the relation
A JOIN B ON a = b.
The field a is the true A's foreing key referred by B's field b and b is B's primary key too. I set the stuff as follow (I use graphic tools):
cdsB.MasterSource := dsA;
cdsB.MasterFields := a;
cdsB.IndexFieldNames := b;
When I do cdsB.Open, I got this error:
ORA-01036: illegal variable name/number".
The field a value is always null in table A (there is no data). TSQLMonitor reports the following queries:
Execute: select * from A
...
Execute: select * from ENTI where (b is NULL)
:1 (Number,IN) = <NULL>
What did I miss, and how can this be fixed?
When using Datasnap, you should set the M/D relationship on the source datasets, not the client ones. It will create a "dataset field" in the master client dataset. You then assign this field to the child client dataset. This approach is also more perfomant.
Anyway it should work as well, it looks there is something wrong with your SQL.

Resources