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

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.

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.

JOOQ: Querying similar tables

I have a situation where I have 2 tables that are almost identical. One table is used for making viewing and editing the data and then the data is published. When the data is published it goes into another table. Essentially WIDGET and PUBLISHED_WIDGET
I have implemented all sorts of custom searching, sorting, filtering and paging queries for 1 table and now I have to implement it for the other. I'm trying to find out a way I can abstract it out and use TableLike<COMMON_WIDGET>.
Example:
create table widget (
id int not null auto_increment,
name varchar(64) not null,
lang varchar(2),
updated_by varchar(64),
updated_on timestamp
//...
);
create table published_widget (
id int not null auto_increment,
name varchar(64) not null,
lang varchar(2),
updated_by varchar(64),
updated_on timestamp
//...
);
I want to be able to do something like this:
public class WidgetDao {
private final TableLike<CommonWidget> table;
public Widget find(String rsql) {
dslContext.selectFrom(table)
.where(table.ID.eq("...").and(table.NAME.eq("...")
// ...
}
Is this possible?
Table mapping
You can use the runtime table mapping feature for this. Choose one of your tables as your "base table" (e.g. WIDGET), and then use a derived configuration with this Settings:
Settings settings = new Settings()
.withRenderMapping(new RenderMapping()
.withSchemata(
new MappedSchema().withInput("MY_SCHEMA")
.withTables(
new MappedTable().withInput("WIDGET")
.withOutput("PUBLISHED_WIDGET"))));
And then:
public Widget find(String rsql) {
// Alternatively, store this derived Configuration in your DAO for caching purposes
dslContext.configuration()
.derive(settings)
.dsl()
.selectFrom(WIDGET)
.where(WIDGET.ID.eq("...").and(WIDGET.NAME.eq("..."))
.fetch();
// ...
}
Such Settings will rename (not alias) the table globally, for a Configuration
Table.rename()
Generated tables have a rename() operation on them, that allows you to do exactly what you want on an ad-hoc basis, not globally. Depending on your use-case, that might be more suitable. Again, this is not the same thing as aliasing (which affects generated SQL).
And again, you'll pick one of your similar/identical tables as your base table, and rename that for your purposes:
public Widget find(String rsql) {
Widget table = WIDGET.rename(PUBLISHED_WIDGET.getQualifiedName());
dslContext.selectFrom(table)
.where(table.ID.eq("...").and(table.NAME.eq("..."))
.fetch();
// ...
}
This method currently (jOOQ 3.14) only exists on generated tables, not on org.jooq.Table, see: https://github.com/jOOQ/jOOQ/issues/5242

Mapping a view with a candidate key that may have nulls in JPA

I have a view on which the only possible candidate key may have a null value. Consider:
CREATE TABLE FOO (
FOO_ID NUMBER(10,0),
CONSTRAINT PK_FOO PRIMARY KEY (FOO_ID)
);
CREATE TABLE BAR (
BAR_ID NUMBER(10,0),
FOO_ID NUMBER(10,0),
CONSTRAINT PK_BAR PRIMARY KEY (BAR_ID),
CONSTRAINT FK_BAR_FOO FOREIGN KEY (FOO_ID) REFERENCES FOO(FOO_ID)
) ;
-- I'm interested in this view!
CREATE VIEW VW_FOO_BAR AS
SELECT FOO.FOO_ID, BAR.BAR_ID
FROM FOO
LEFT JOIN BAR ON FOO.FOO_ID = BAR.FOO_ID
Needless to say, this is only a MCVE. The actual DDL is much more complex.
Note that, in this view, BAR_ID may be null, since it's a LEFT JOIN. But FOO_ID may be repeated multiple times, so it can't be the key by itself. As such, the only possible candidate key is (FOO_ID, BAR_ID)
It's not possible to make any changes on the database or its views. Also, this view has some important information that is only accessible through it, so I have to use it.
Since it's a view, I will never be making insertions or updates on it. Only queries.
Question:
Is there a way to map this view in JPA, without triggering the infamous "More than one row with the given identifier" exception?
Discarded solutions:
This implementation may return multiple rows with the same identifier, causing the above mentioned exception:
#Entity
#Table(name = "VW_FOO_BAR")
class VwFooBar {
#Id
#Column(name="FOO_ID")
private Long idFoo;
#Column(name="BAR_ID")
private Long idBar
}
This one will return null objects whenever idBar is null:
#Entity
#Table(name = "VW_FOO_BAR")
class VwFooBar {
#EmbeddedId
private VwFooBarPk pk;
}
#Embeddable
class VwFooBarPk {
#Column(name="FOO_ID")
private Long idFoo;
#Column(name="BAR_ID")
private Long idBar
}
I'm accepting any solution that allows me to query the view, altough I prefer to avoid pure JDBC, if possible.
I'm using JPA 1/Hibernate 3.3 with the org.hibernate.dialect.Oracle9iDialect dialect.

History, Diff and reverts of persisted objects

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.

MYSQL stored procedure accessing java object stored as a BLOB

I am storing a Java object as an byte in a blob of a table. The java object is customized object. How can I construct the java object and use it in the stored procedure?
Let the class implement java.io.Serializable so that you can get an InputStream of it which you can store in the DB using CallableStatement#setBinaryStream().
That said, this is usually considered a bad design. If the class is actually a Javabean class, you'd better create a table with columns which represents the Javabean properties. E.g. a public class User { private Long id; private String name; private Integer age; } should be mapped to a table like CREATE TABLE user ( id BIGINT AUTO_INCREMENT, name VARCHAR, age INTEGER )
Edit as a reply on your comment: you thus basically want to store an array as binary object. This is a very bad idea. This way you cannot search for the array's data in the database and the database would also not be portable anymore. Just create a new table which represents each of the array items. Add an extra column to it which represents the ID of the parent object (actually, it should be the PK of the table to which the parent object containing the array is been mapped.
Example:
public class Parent {
private Long id;
private String someData;
private List<Child> children;
// Add/generate public getters/setters.
}
public class Child {
private Long id;
private String someData;
// Add/generate public getters/setters.
}
should be mapped to
CREATE TABLE parent (
id BIGINT NOT NULL AUTO_INCREMENT,
someData VARCHAR,
PRIMARY KEY (id)
);
CREATE TABLE child (
id BIGINT NOT NULL AUTO_INCREMENT,
parent_id BIGINT NOT NULL,
someData VARCHAR,
PRIMARY KEY (id),
FOREIGN KEY (parent_id) REFERENCES parent(id)
);
this way you can just select all with help of a JOIN clause. Check the SQL tutorial at w3schools.com and the vendor-specific SQL documentation for examples.
How can I construct the java object and use it in the stored procedure?
This is not possible, at least not with MySQL. Unlike Oracle which supports Java Stored Procedures, MySQL's stored procedure syntax is based on plain ANSI SQL standard. So I don't see how you could construct a Java object from the stream stored in the BLOB. What you can do is acces to the BLOB, but this won't help you much IMHO.
Actually, I think you are totally on the wrong path here, using a BLOB is not the right way to go (at least not here). If you need to persist objects that have a 1:n relation between them, you need to model your database accordingly.
If your Record class has a one to many relation with the User class, which is my understanding, then you have something like this on the Java side:
public class Record {
private Long id;
private User[];
//...
}
Then you need to create two tables at the database level, one for the records and another for the user(s), and model the relation between them using a foreign key (so you can "attach" a user to a record):
CREATE TABLE record
(
record_id INT NOT NULL,
...,
PRIMARY KEY (record_id)
) TYPE = INNODB;
CREATE TABLE user
(
user_id INT NOT NULL,
record_id INT,
...
PRIMARY KEY (user_id),
INDEX (record_id),
FOREIGN KEY (user_id) REFERENCES record (record_id)
) TYPE = INNODB;
Finally, when persisting a Record instance from Java, you'll need to write state in both tables.

Categories