Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

Versioned REST-Api with CQRS - How to best use Jooq in this Scenario

18 views
Skip to first unread message

Bernd Huber

unread,
Oct 13, 2024, 4:16:03 AM10/13/24
to jOOQ User Group
Hello guys,

i really loved the following video by Victor Rentea about REST API Design Pitfalls.

in my company we also need to build a Versioned REST-Api, which some specific Clients can use in a secure way.

Can someone recommend me an Article or Best-Practice how to integrate Jooq in such a scenario ?

I can think of something like ...
- i manually create versioned DTOs (ProductDTOV1, ProductDTOV2, ...) as simple Pojos.
- i manually write Insert / Update / Delete Methods in my DAOs where i need to boilerplate down all the fields i want to consider for each specific case.
- i write a Repository where i Select data and also concretely (boilerplate) map each field i select into the versioned DTO.
- the Jooq-Codegen-Classes are only used in internal Jobs where REST is not involved, as they are just an unnecessary middle-man for the REST use-cases.

I know that Jooq does support everything i want to do with it, but I often tend to overabstract, and i hope to find a simple way (even if its boilerplate) to write REST-Apis with jooq, that do not involve overabstracted solutions where they are counterproductive.

I guess the outline i have shown here should work.

best regards,

Bernd Huber

Simon Martinelli

unread,
Oct 13, 2024, 6:14:26 AM10/13/24
to jooq...@googlegroups.com
Hi Bernd,

If you need to support mulitple versions of your API and this only involves the projection part of your query, then you must create a projection per DTO (preferably as Java Record). 
That could be abstracted in versioned repositories (DAOs).

There would also be a more generic approach to derive the projection directly from the constructor of the Java Record. But this would require reflection and is not compile-time checked.

That’s how I would do it.

Kind regards, 
Simon



--
You received this message because you are subscribed to the Google Groups "jOOQ User Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jooq-user+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/jooq-user/ac30997e-16c1-4b74-adb7-03ebf8487255n%40googlegroups.com.

Bernd Huber

unread,
Oct 13, 2024, 8:50:36 AM10/13/24
to jOOQ User Group
Hello Simon,

thx for answering!

sounds good, i think i will go this way.
Java-Records are really nice / concise and also have toString, equals, hashCode which can also be helpful.

I see two ways where the projection can be build with:
- a) ... on the Jooq-Records (Jooq-Record Classes generated by Codegen)
- b) ... directly on the Jooq DSL

i think you have meant b) in your example. And i will also go that way i think,
Sometimes it can also be good to go with a) because then at least Bulk/Batch Inserts and similar performance-optimizations can be done in an AbstractDAO for the inserts.
But b) is simpler (has less code-parts / more concise)

Here an example for a) and for a REST Create Endpoint.
Each endpoint would have individual DTOs for Reqest/Response, as defined by CQRS.

---

ProductControllerV1

    public CreateProductResponseV1 create(CreateProductRequestV1 createProductRequest) throws ValidationException {
        return productManager.create(new RequestContext(1, 1), createProductRequest);
    }

---

ProductManagerV1

    public CreateProductResponseV1 create(RequestContext requestContext, final CreateProductRequestV1 createProductRequest) throws ValidationException {
        return database1.dsl(requestContext).transactionResult(tsx -> {
            ProductDAOV1 productDAOV1 = new ProductDAOV1(tsx.dsl());

            this.validate(createProductRequest);

            // request-dto to jooq-record projection
            ProductRecord insert = new ProductRecord();
            insert.setClientId(createProductRequest.clientId());
            insert.setPrice(createProductRequest.price());
            insert.setTypeId(createProductRequest.typeId());
            insert.setDeleted(false);

            productDAOV1.insert(insert);

            ProductRecord result = productDAOV1.fetch(insert.getProductId());

            // jooq-record to response-dto projection
            return new CreateProductResponseV1(
                result.getProductId(),
                result.getClientId(),
                result.getPrice(),
                result.getTypeId(),
                result.getCreatedAt(),
                result.getUpdatedAt(),
                result.getDeleted(),
                result.getCreatorId()
            );
        });
    }

---

CreateProductRequestV1

public record CreateProductRequestV1(
    @NotNull Integer clientId,
    @NotNull BigDecimal price,
    @NotNull @Size(max = 255) String typeId
) {
}

---

CreateProductResponseV1

public record CreateProductResponse(
    @NotNull Long productId,
    @NotNull Integer clientId,
    @NotNull BigDecimal price,
    @NotNull @Size(max = 255) String typeId,
    @NotNull LocalDateTime createdAt,
    @NotNull LocalDateTime updatedAt,
    @NotNull Boolean deleted,
    @NotNull Integer creatorId
) {
}

---

additional, if the DAO is used for the projection (like you suggested) the DAO insert-method could look like this,
for example:

ProductDAOV1

public class ProductDAOV1 extends AbstractDAO<ProductRecord, Long> {
   ...
    int insertProduct(CreateProductRequestV1 createProductRequest) {
        return dsl()
            .insertInto(PRODUCT)
            .set(PRODUCT.CLIENTID, createProductRequest.clientId())
            .set(PRODUCT.PRICE, createProductRequest.price())
            .set(PRODUCT.TYPEID, createProductRequest.typeId())
            .set(PRODUCT.DELETED, false)
            .execute();
    }

Bernd Huber

unread,
Oct 13, 2024, 9:12:56 AM10/13/24
to jOOQ User Group

also a complete projection in the DAO could look like this,
and if Get-Queries are done it would also be necessary to make projections in MULTISET with help of convertFrom (nested convertFrom for table-joins)

ProductDAOV1

    public CreateProductResponseV1 create(CreateProductRequestV1 createProductRequest) {

        // project request to jooq-query.
        Long productId = dsl()

            .insertInto(PRODUCT)
            .set(PRODUCT.CLIENTID, createProductRequest.clientId())
            .set(PRODUCT.PRICE, createProductRequest.price())
            .set(PRODUCT.TYPEID, createProductRequest.typeId())
            .set(PRODUCT.DELETED, false)
            .returning(PRODUCT.PRODUCTID)
            .fetchOne(PRODUCT.PRODUCTID);

        // get db-content for id, and project to response...
        Record1<CreateProductResponse> result = dsl()
            .select(
                row(
                    PRODUCT.PRODUCTID,
                    PRODUCT.CLIENTID,
                    PRODUCT.PRICE,
                    PRODUCT.TYPEID,
                    PRODUCT.CREATEDAT,
                    PRODUCT.UPDATEDAT,
                    PRODUCT.DELETED,
                    PRODUCT.CREATORID
                ).convertFrom(x -> x.into(CreateProductResponseV1.class))
            )
            .from(PRODUCT)
            .where(PRODUCT.PRODUCTID.eq(productId))
            .fetchOne();

        if (result != null) {
            return result.value1();
        } else {
            throw new MappingException("internal error");
        }
    }

Rob Sargent

unread,
Oct 13, 2024, 2:05:40 PM10/13/24
to jooq...@googlegroups.com
What does ProductDaoV2 look like?

I assume the client knows which version it wants?

Bernd Huber

unread,
Oct 13, 2024, 2:34:34 PM10/13/24
to jOOQ User Group
Hello Rob,

the client would be informed about newer versions,
and a new version would be the result of communication about....
- ... desired new parts in the API that are still missing
- .... resulting of refactoring from our side, where the api could be optimized etc.

The version is encoded in the Path of the Controller and the client can choose which version he wants to call.
(old versions will be removed after some releases)

ProductControllerV1

@Path("/api/v1/products")
public class ProductControllerV1 {

    @Path("/")
  public CreateProductResponseV1 create(CreateProductRequestV1 createProductRequest) throws ValidationException {
     return productManager.create(new RequestContext(1, 1), createProductRequest);
  }
}

---

The ProductDaoV1 was only an example how such a versioned Dao could look like.

A ProductDaoV2 could look exactly the same like ProductDaoV1 with only minor changes, like:
- a new field: PRODUCT.NAME that was missing in V1 and is now desired by the client.
- a removed field
- a renamed field
- a changed datatype
- ...

It is a hassle for the programmer who needs to copy all the V1 code to V2 (all DAOs, DTOs, Repositories, Controllers, ... that are in V1),
and rename all those to V2 and then only change some small parts, but at the same time there is the desire to be able
to have older versions still available, while migrating to a new version (clients need to be convinced to migrate)

Bernd Huber

unread,
Oct 17, 2024, 7:19:06 AM10/17/24
to jOOQ User Group
to give an update.

I think i will also try out "Lombok" now, while i often hesitated to do so.

- Java Records are great to use, because they are short/concise
- The immutability of Java Records can be hard to work with when having to work with legacy code (mappings from unclean db to clean api-model are necessary here, and immutability makes it harder for this).
- Java Records have no Builder-Pattern, the only have the constructor, and when constructing them manually it is harder to maintain / read.

Lombok has a @Builder annotation for Java Records, i will try it :)

While Java Records are great to have, i think they still lack some small details,
and i may fall back to regular Pojos for the Api

But currently everything looks good with the general concept for the api.

Bernd Huber

unread,
Oct 18, 2024, 8:09:47 AM10/18/24
to jOOQ User Group
again an update :)

lombok somehow works, but its approach crumbles when wanting to support REST-Patch Method fully,
because then hibernate-validator does not really works with "required" vs "not-required" fields like REST-Patch allows. 
I got stuck here in my thoughts.

but i think i will just go for a HashMap instead of a Java-Record / Model for REST-Patch as Body, as someone pointed me in following stackoverflow:

Java Ecosystem seems to have neglected REST-Patch a bit, in regards to support...
Reply all
Reply to author
Forward
0 new messages