History, Diff and reverts of persisted objects - java

In a Spring MVC / Spring Data project I need to implement a mechanism to track history, present differences and revert the changes to an entity object.
Let's say I have an entity with relationships with others like this:
#Entity
public Class ModelA{
#OneToOne(cascade = CascadeType.ALL)
private ModelB modelB;
#OneToOne(cascade = CascadeType.ALL)
private ModelC modelC;
}
I want to have the list of changes, the ability to compare and revert them. I know that using Ruby there are libs that provide this kind of functionality, but I'm not aware if such thing exist in Java.
Spring has a historiography API and Hibernate Envers had been incorporated in Core functionality, although I still can't find a simple example or some guidance how to implement it.
If it's relevant the used database is PostgreSQL and Oracle 11g, but I want to keep it database independent.

Use Enver and Auditions instead please.

One very interesting approach is given by Christian Bauer (Hibernate committer and author of Hibernate in Action and Java Persistence with Hibernate) in this post.
You create a HISTORY table:
create table ITEM (
ITEM_ID NUMBER(19) NOT NULL,
DESC VARCHAR(255) NOT NULL,
PRICE NUMBER(19,2) NOT NULL,
PRIMARY KEY(ITEM_ID)
)
create table ITEM_HISTORY (
ITEM_ID NUMBER(19) NOT NULL,
DESC VARCHAR(255) NOT NULL,
PRICE NUMBER(19,2) NOT NULL,
VERSION NUMBER(10) NOT NULL,
PRIMARY KEY(ITEM_ID, VERSION)
)
Then you map entities to a view instead:
create or replace view ITEM_VERSIONED (ITEM_ID, VERSION, DESC, PRICE) as
select I.ITEM_ID as ITEM_ID,
(select max(IH.VERSION)
from ITEM_HISTORY HI
where HI.ITEM_ID = I.ITEM_ID) as VERSION,
I.DESC as DESC,
I.PRICE as PRICE
from ITEM I
and the DML statements are resolved by INSTEAD OF TRIGGERS which are supported by PostgreSQL and Oracle:
create or replace trigger ITEM_INSERT
instead of insert on ITEM_VERSIONED begin
insert into ITEM(ITEM_ID, DESC, PRICE)
values (:n.ITEM_ID, :n.DESC, :n.PRICE);
insert into ITEM_HISTORY(ITEM_ID, DESC, PRICE, VERSION)
values (:n.ITEM_ID, :n.DESC, :n.PRICE, :n.VERSION);
end;
create or replace trigger ITEM_UPDATE
instead of update on ITEM_VERSIONED begin
update ITEM set
DESC = :n.DESC,
PRICE = :n.PRICE,
where
ITEM_ID = :n.ITEM_ID;
insert into ITEM_HISTORY(ITEM_ID, DESC, PRICE, VERSION)
values (:n.ITEM_ID, :n.DESC, :n.PRICE, :n.VERSION);
end;
This will work even for other applications that may not use Hibernate, yet they operate on the same DB.

If I understand well, what you ask is some sort of Memento pattern to manage some entities subject to history tracking.
In this case, my suggestion is to configure Spring Data to support a second database (i.e. a tracking database), where you will insert the history of the entities you are interested in.
Then you may create a new annotation (possibly using AspectJ) and apply it to your DAOs (e.g. to your repositories, if you are using them). This way, every time you make a CRUD operation on a tracked class (or, more precisely, on a dao/repository that manages a class you want to track), you make an "insert" in the tracking database storing the change that just occurred.
I can give you this reference, which does not match exactly your need, but may support you at finding the one solution that solves your issue.

Related

JOOQ arrayAgg array_agg fields as object

I have 2 entities:
record Customer(String name, List<CustomerContact > contactHistory) {}
record CustomerContact(LocalDateTime contactAt, Contact.Type type) {
public enum Type {
TEXT_MESSAGE, EMAIL
}
}
These are persisted in a schema with 2 tables:
CREATE TABLE customer(
"id". BIGSERIAL PRIMARY KEY,
"name" TEXT NOT NULL
);
CREATE TABLE customer_contact(
"customer_id" BIGINT REFERENCES "customer" (ID) NOT NULL,
"type" TEXT NOT NULL,
"contact_at" TIMESTAMPTZ NOT NULL DEFAULT (now() AT TIME ZONE 'utc')
);
I want to retrieve the details of my Customers with a single query, and use the arrayAgg method to add the contactHistory to each customer. I have a query like this:
//pseudo code
DSL.select(field("customer.name"))
.select(arrayAgg(field("customer_contact.contact_at")) //TODO How to aggregate both fields into a CustomerContact object
.from(table("customer"))
.join(table("customer_contact")).on(field("customer_contact.customer_id").eq("customer.id"))
.groupBy(field("customer_contact.customer_id"))
.fetchOptional()
.map(asCustomer());
The problem I have with this is that arrayAgg will only work with a single field. I want to use 2 fields, and bind them into a single object (CustomerContact) then use that as the basis for the arrayAgg
Apologies if I have not explained this clearly! Any help much appreciated.
Rather than using ARRAY_AGG, how about using the much more powerful MULTISET_AGG or MULTISET to get the most out of jOOQ's type safe capabilities? Combine that with ad-hoc conversion for type safe mapping to your Java records, as shown also in this article. Your query would then look like this:
Using MULTISET_AGG
List<Customer> customers =
ctx.select(
CUSTOMER.NAME,
multisetAgg(CUSTOMER_CONTACT.CONTACT_AT, CUSTOMER_CONTACT.TYPE)
.convertFrom(r -> r.map(Records.mapping(CustomerContact::new))))
.from(CUSTOMER)
.join(CUSTOMER_CONTACT).on(CUSTOMER_CONTACT.CUSTOMER_ID.eq(CUSTOMER.ID))
.groupBy(CUSTOMER_CONTACT.CUSTOMER_ID)
.fetch(Records.mapping(Customer::new));
Note that the entire query type checks. If you change anything about the query or about your records, it won't compile anymore, giving you additional type safety. This is assuming that youre Type enum is either:
Generated from a PostgreSQL ENUM type
Converted automatically using an enum converter, attached to generated code
Depending on your tastes, using implicit joins could slightly simplify the query for you?
List<Customer> customers =
ctx.select(
CUSTOMER_CONTACT.customer().NAME,
multisetAgg(CUSTOMER_CONTACT.CONTACT_AT, CUSTOMER_CONTACT.TYPE)
.convertFrom(r -> r.map(Records.mapping(CustomerContact::new))))
.from(CUSTOMER_CONTACT)
.groupBy(CUSTOMER_CONTACT.CUSTOMER_ID)
.fetch(Records.mapping(Customer::new));
It's not a big deal in this query, but in a more complex query, it can reduce complexity.
Using MULTISET
An alterantive is to nest your query instead of aggregating, like this:
List<Customer> customers =
ctx.select(
CUSTOMER.NAME,
multiset(
select(CUSTOMER_CONTACT.CONTACT_AT, CUSTOMER_CONTACT.TYPE)
.from(CUSTOMER_CONTACT)
.where(CUSTOMER_CONTACT.CUSTOMER_ID.eq(CUSTOMER.ID))
).convertFrom(r -> r.map(Records.mapping(CustomerContact::new))))
.from(CUSTOMER)
.fetch(Records.mapping(Customer::new));
Code generation
For this answer, I was assuming you're using the code generator (you should!), as it would greatly contribute to this code being type safe, and make this answer more readable.
Much of the above can be done without code generation (except implicit joins), but I'm sure this answer could nicely demonstrate the benefits it terms of type safety.

How to use the same Java class with different SQL queries

I'm designing a Java report application that can receive any SQL query(on a already defined database) and then I display the result in a table grid in a Vue.js application.
I have some doubts on how to load a Java generic/hybrid class that can fit differents SQL queries.
I mean, is it possible to create a class in Java that can change/mutate during runtime so I can map different SQL queries in it?
I know that it is possible to use the java.sql.ResultSetMetaData to get the column name, table name, column class name, etc. (I don't know if it possible with hibernate)
But I don't know how to map the results in a unique class.
For example:
I have 3 differents queries and they could be a lot more:
1) SELECT ID, COUNTRY_NAME FROM COUNTRY;
2) SELECT CODE, NAME, PRICE FROM PRODUCT;
3) SELECT P.CODE, P.NAME, S.NAME
FROM PRODUCT P
JOIN SUPPLIER S
ON S.ID = P.SUPPLIER_ID
WHERE P.PRICE > 25;
I need to map this query results(one at a time obviously) in a generic class so then I pass it to the Vue app to display it in a grid.
Is it a way to do that?
Don't reinvent the wheel and use what is already available and what is well tested.
What you're looking for is called Spring JDBC, or, for an even higher level, Java Persistence API.
Using Spring JDBC to extract a set of records might look like
final List<YourClass> results =
jdbcTemplate.queryForList(
"Native SQL statement",
queryArguments,
YourClass.class
);
Using a JPA implementation (e.g. Hibernate, EclipseLink, ObjectDb), the operation might look like
final TypedQuery<YourClass> query = entityManager.createQuery("JPQL statement", YourClass.class);
final List<YourClass> results = query.getResultList();

JQPL Update Query to update entity without using the primary key

This may be a simple question, but I'm trying to find out if there is a way that I can create a JPQL update query that would allow me to update a single Persisted Entity using a unique column identifier that is not the primary key.
Say I have and entity like the following:
#Entity
public class Customer {
#ID
private Long id;
#Column
private String uniqueExternalID;
#Column
private String firstname;
....
}
Updating this entity with a Customer that has the id value set is easy, however, id like to update this customer entity using the uniqueExternalId without having to pre-query for the local entity and merge the changes in or manually construct a jpql query with all the fields in it manually.
Something like
UPDATE Customer c SET c = :customer WHERE c.uniqueExternalId = :externalId
Is something like this possible in JQPL?
You cannot do it in the exact way you describe - by passing an entity reference, but you can use bulk queries to achieve the same effect.
UPDATE Customer c SET c.name = :name WHERE c.uniqueExternalId = :externalId
Please note that you will have to explicitly define each updated attribute.
It is important to note that bulk queries bypass the persistence context. Entity instances that are managed within the persistence context will not reflect the changes to the records that are changed by the bulk update. Further, if you use optimistic locking, consider incrementing the #Version field of your entities with the bulk update:
UPDATE Customer c SET c.name = :name, c.version = c.version + 1 WHERE c.uniqueExternalId = :externalId
EDIT: The JPA 2.0 spec advises in § 4.10:
In general, bulk update and delete operations should only be performed
within a transaction in a new persistence context or before fetching
or accessing entities whose state might be affected by such
operations.

JOOQ pojos with one-to-many and many-to-many relations

I am struggling to understand how to handle pojos with one-to-many and many-to-many relationships with JOOQ.
I store locations that are created by players (one-to-many relation). A location can hold multiple additional players who may visit it (many-to-many). The database layout comes down to the following:
CREATE TABLE IF NOT EXISTS `Player` (
`player-id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`player` BINARY(16) NOT NULL,
PRIMARY KEY (`player-id`),
UNIQUE INDEX `U_player` (`player` ASC))
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `Location` (
`location-id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(32) CHARACTER SET 'utf8' COLLATE 'utf8_bin' NOT NULL,
`player-id` INT UNSIGNED NOT NULL COMMENT '
UNIQUE INDEX `U_name` (`name` ASC),
PRIMARY KEY (`location-id`),
INDEX `Location_Player_fk` (`player-id` ASC),
CONSTRAINT `fk_location_players1`
FOREIGN KEY (`player-id`)
REFERENCES `Player` (`player-id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `location2player` (
`location-id` INT UNSIGNED NOT NULL,
`player-id` INT UNSIGNED NOT NULL,
INDEX `fk_ location2player_Location1_idx` (`location-id` ASC),
INDEX `fk_location2player_Player1_idx` (`player-id` ASC),
CONSTRAINT `fk_location2player_Location1`
FOREIGN KEY (`location-id`)
REFERENCES `Location` (`location-id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_location2player_Player1`
FOREIGN KEY (`player-id`)
REFERENCES `Player` (`player-id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
Within my java application, all these informations are stored within one pojo. Note that the player and the list of invited players can be updated from within the application and need to be updated in the database as well:
public class Location {
private final String name;
private UUID player;
private List<UUID> invitedPlayers;
public void setPlayer(UUID player) {
this.player = player;
}
public void invitePlayer(UUID player) {
invitedPlayers.add(player);
}
public void uninvitePlayer(UUID player) {
invitedPlayers.remove(player);
}
//additional methods…
}
Can I use JOOQ’s pojo mapping to map these three records into the single pojo? Can I use JOOQ’s CRUD feature from this pojo to update the one-to-many and many-to-many relations? If the pojo mapping cannot be used, can I take advantage of JOOQ in any way except using it to write my SQL statements?
Using MULTISET for nested collections with jOOQ 3.15
Starting from jOOQ 3.15, you can use the standard SQL MULTISET operator to nest collections, and to abstract over the below SQL/XML or SQL/JSON serialisation format. Your query would look like this:
List<Location> locations
ctx.select(
LOCATION.NAME,
LOCATION.PLAYER,
multiset(
select(LOCATION2PLAYER.PLAYER_ID)
.from(LOCATION2PLAYER)
.where(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
).as("invitedPlayers")
)
.from(LOCATION)
.fetchInto(Location.class);
If your DTOs were immutable (e.g. Java 16 records), you can even avoid using reflection for mapping, and map type safely into your DTO constructors using constructor references and the new jOOQ 3.15 ad-hoc conversion feature.
List<Location> locations
ctx.select(
LOCATION.NAME,
LOCATION.PLAYER,
multiset(
select(LOCATION2PLAYER.PLAYER_ID)
.from(LOCATION2PLAYER)
.where(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
).as("invitedPlayers").convertFrom(r -> r.map(Record1::value1))
)
.from(LOCATION)
.fetch(Records.mapping(Location::new));
See also this blog post for more details about MULTISET
Using SQL/XML or SQL/JSON for nested collections with jOOQ 3.14
Starting from jOOQ 3.14, it's possible to nest collections using SQL/XML or SQL/JSON, if your RDBMS supports that. You can then use Jackson, Gson, or JAXB to map from the text format back to your Java classes. For example:
List<Location> locations
ctx.select(
LOCATION.NAME,
LOCATION.PLAYER,
field(
select(jsonArrayAgg(LOCATION2PLAYER.PLAYER_ID))
.from(LOCATION2PLAYER)
.where(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
).as("invitedPlayers")
.convertFrom(r -> r.map(Records.mapping(Pla)
)
.from(LOCATION)
.fetch(Records.mapping(Location::new));
In some database products, like PostgreSQL, you could even use SQL array types using ARRAY_AGG() and skip using the intermediate XML or JSON format.
Note that JSON_ARRAYAGG() aggregates empty sets into NULL, not into an empty []. If that's a problem, use COALESCE()
Historic answer (pre jOOQ 3.14)
jOOQ doesn't do this kind of POJO mapping out of the box yet, but you can leverage something like ModelMapper which features a dedicated jOOQ integration, which works for these scenarios to a certain extent.
Essentially, ModelMapper hooks into jOOQ's RecordMapper API. More details here:
http://www.jooq.org/doc/latest/manual/sql-execution/fetching/recordmapper/
http://www.jooq.org/doc/latest/manual/sql-execution/fetching/pojos-with-recordmapper-provider/
You can use SimpleFlatMapper on the
ResultSet of the query.
create a mapper with player as the key
JdbcMapper<Location> jdbcMapper =
JdbcMapperFactory.addKeys("player").newMapper(Location.class);
Then use fetchResultSet to get the ResultSet and pass it to the mapper.
Note that it is important to orderBy(LOCATION.PLAYER_ID) otherwise you might end up with split Locations.
try (ResultSet rs =
dsl
.select(
LOCATION.NAME.as("name"),
LOCATION.PLAYER_ID.as("player"),
LOCATION2PLAYER.PLAYERID.as("invited_players_player"))
.from(LOCATION)
.leftOuterJoin(LOCATION2PLAYER)
.on(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
.orderBy(LOCATION.PLAYER_ID)
.fetchResultSet()) {
Stream<Location> stream = jdbcMapper.stream(rs);
}
then do what you need to do on the stream, you can also get an iterator.

How to handle internationalization of names in Hiberante and db schema

I saw a few questions in stackoverflow, but they all refer to old answers (from 2004) and using hibernate xml mappings.
I am writing an API in Java, which will return products which are stored in the database based on an algorithm.
The API would also get a locale and decide to return the name in the locale language.
Product table:
product_id
name
price
How should I support internalization?
Should I add name_en name_fr etc columns?
Or should I create different tables?
How should I return the name in the required locale in Hibernate?
I am using annotations for O/R mapping
I would suggest adding a column "lang" that contains prefix of language (ex. en, fr, ro, etc.) and a column "name" that contains the name in all languages separated by a separator like "," and parse the string.
I would remove the name column, and create a one-to-many relationship with a separate table named localized_name:
create table localized_name (
product_key int not null,
locale nvarchar(32) not null,
name nvarchar(255) not null
)
My experience is with JPA, not Hibernate, but I'm guessing the code looks similar in both:
public class Product {
private Collection<LocalizedName> names;
}
In JPA I would add a #Size(min = 1) annotation to the field to ensure it contains at least one element when it is persisted.

Categories