Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

select into insert from values() with correct type casts using jOOQ

I'm using jOOQ to insert quite a few rows into a table that is a many-to-many relation. The code works, the generated SQL is as expected, my problem is that I would hope the jOOQ code could be simpler.

A simplified structure I have (everything renamed, most fields removed, most constraints removed, it's just a silly, but accurate example of the structure):

CREATE TABLE person (
    person_id BIGSERIAL PRIMARY KEY,
    person_name VARCHAR(64) NOT NULL UNIQUE
);

CREATE TABLE company (
    company_id BIGSERIAL PRIMARY KEY,
    company_name VARCHAR(100) NOT NULL UNIQUE
);

CREATE TABLE employment_contract (
    company_id BIGINT NOT NULL REFERENCES company,
    person_id BIGINT NOT NULL REFERENCES person,
    PRIMARY KEY (company_id, person_id),

    salary INT NOT NULL,
    creation_date_time TIMESTAMP NOT NULL
);

My insertion code:

Table<Record4<String, String, Integer, Timestamp>> insertValues = values(
    row(
        cast(null, COMPANY.COMPANY_NAME),
        cast(null, PERSON.PERSON_NAME),
        cast(null, EMPLOYMENT_CONTRACT.SALARY),
        cast(null, EMPLOYMENT_CONTRACT.CREATION_DATE_TIME)
    )
).as("insert_values",
        COMPANY.COMPANY_NAME.getName(),  -- these lines are bugging me
        PERSON.PERSON_NAME.getName(),
        EMPLOYMENT_CONTRACT.SALARY.getName(),
        EMPLOYMENT_CONTRACT.CREATION_DATE_TIME.getName()
);

Insert<AffectedSubscriberRecord> insert = insertInto(EMPLOYMENT_CONTRACT)
    .columns(EMPLOYMENT_CONTRACT.COMPANY_ID,
            EMPLOYMENT_CONTRACT.PERSON_ID,
            EMPLOYMENT_CONTRACT.SALARY,
            EMPLOYMENT_CONTRACT.CREATION_DATE_TIME
    )
    .select(
        select(
            COMPANY.COMPANY_ID,
            PERSON.PERSON_ID,
            insertValues.field(EMPLOYMENT_CONTRACT.SALARY),
            insertValues.field(EMPLOYMENT_CONTRACT.CREATION_DATE_TIME)
        )
        .from(insertValues)
            .join(COMPANY).using(COMPANY.COMPANY_NAME)
            .join(PERSON).using(PERSON.PERSON_NAME)
    );

Then I bind all my rows to context.batch(insert) and execute the thing. I know for sure the referenced keys for person and company already exist, and the original code also resolves duplicates, we do not need to care about those things here.

What bugs me is the insertValues table - I need to specify the column types and names twice, in an error-prone copy-paste, using .getName() calls that obscure the whole code and are easy to swap by mistake. What I tried instead:

Table<Record4<String, String, Integer, Timestamp>> insertValues = values(
    row( (String)null, (String)null, (Integer)null, (Timestamp)null )
).as("insert_values",
        COMPANY.COMPANY_NAME.getName(),
        PERSON.PERSON_NAME.getName(),
        EMPLOYMENT_CONTRACT.SALARY.getName(),
        EMPLOYMENT_CONTRACT.CREATION_DATE_TIME.getName()
);

This obviously does not work, neither jOOQ nor Postgres knows the inserted types, the DB guesses varchar and fails. We need jOOQ to generate typecasts at least for the first row in the query. Another try:

Table<Record4<String, String, Integer, Timestamp>> insertValues = values(
    row( COMPANY.COMPANY_NAME, PERSON.PERSON_NAME, EMPLOYMENT_CONTRACT.SALARY, EMPLOYMENT_CONTRACT.CREATION_DATE_TIME )
).as("insert_values");

This would be the bomb. JOOQ knows this way the correct types and could generate the casts for me, all code duplication disappears and things are safe. However, this fails, too. JOOQ does not understand that I'm giving it a row full of nulls.

Is there any way to achieve the same (or equivalent) resulting query without the unclean .getName() calls, directly passing the fields somewhere?

like image 739
Petr Janeček Avatar asked Sep 10 '17 06:09

Petr Janeček


1 Answers

So values() in jOOQ gives you a table constructor. By default select() gives you all columns as they are, which means jOOQ will define as Field<object>

I think the way to think about this is that jOOQ at this point is to recognise two things. You can try to define fields in the output or you can accept objects and try to handle type cases on the way back. My preference would be for fields in the output.

One important aspect to consider here is that rows have to have the same types for a given column but there are cases where implicit casts in SQL may mess with that in jOOQ. For example, if you insert a numeric value in the first row and an integer in the second, the integer will be cast to numeric implicitly. Defining fields for the select avoids this problem. So in terms of the question I would define the fields separately and specifically them in the select from values section. That should solve that problem. With VALUES(), you are already on your own and jOOQ cannot infer types safely.

So with this, your code should become:

Table<Record4<String, String, Integer, Timestamp>> insertValues = values(
    row(
        cast(null, COMPANY.COMPANY_NAME),
        cast(null, PERSON.PERSON_NAME),
        cast(null, EMPLOYMENT_CONTRACT.SALARY),
        cast(null, EMPLOYMENT_CONTRACT.CREATION_DATE_TIME)
    )
).as("insert_values", "company_name", "person_name", "salary", "creation_time");
Field<String> ivCompanyNmae = field("insert_values.company_name". Stirng.class);
Field<Integer> ivSalary = field("insert_values.salary", Integer.class);
...

Insert<AffectedSubscriberRecord> insert = insertInto(EMPLOYMENT_CONTRACT)
    .columns(EMPLOYMENT_CONTRACT.COMPANY_ID,
        EMPLOYMENT_CONTRACT.PERSON_ID,
        EMPLOYMENT_CONTRACT.SALARY,
        EMPLOYMENT_CONTRACT.CREATION_DATE_TIME
    )
    .select(
        select(
            COMPANY.COMPANY_ID,
            PERSON.PERSON_ID,
            ivSalary,
            ivCreatedTime
        )
        .from(insertValues)
            .join(COMPANY).using(COMPANY.COMPANY_NAME)
            .join(PERSON).using(PERSON.PERSON_NAME)
     );

It's this point where jOOQ generates the casts. Unknowns can become archers but will then be explicitly cast correctly.

like image 56
Chris Travers Avatar answered Oct 06 '22 00:10

Chris Travers