Remove foreign keys in custom code generator

Viewed 43

I'm writing a custom code generator where each master data table has its own java enum. I use the forcedType settings in the JOOQ configuration to map every use of the table to the enum, which works fine. I'm working on removing the unnecessary generated classes like the POJO, DAO, and Record and came up with 2 options.

  1. Exclude table from code generation

Using the <excludes>table_name_which_is_an_enum_in_java</excludes> flag however this also messes up the code generation for my enum, because the tables in the schema do not contain the excluded table anymore meaning I cannot convert it to an enum.

  1. Manually skip code generation in custom generator

The idea is to check every relevant generation method and skip the table if it's the table which should be converted to an enum. This works fine for

  • POJOs with generatePOJO
  • DAOs with generateDAO
  • Records with generateRecords

To prevent the table generation I remove the table from the database object (to my surprise the returned list is mutable).

protected void generateSchema(SchemaDefinition schema) {
    //enum generation logic
    List<TableDefinition> tableDefinitions = db.getTables(schema);
    TableDefinition toRemove = db.getTable(schema, "table_name_which_is_an_enum_in_java", true);
    boolean removed = tableDefinitions.remove(toRemove);
    if(removed) {
        log.info("Removed table {} from code generation", toRemove);
    }
}

But I have trouble removing the foreign key relations. When I override printForeignKey and only call the parent method if the foreign key's referenced table is not the table which should be converted to an enum, then the generated Keys class does not generate the foreign key, however it still attemps to import the record, which does not exist.

@Override
protected void printForeignKey(JavaWriter out, int foreignKeyCounter, ForeignKeyDefinition foreignKey, boolean distributeForeignKey) {
    if(foreignKey.getReferencedTable().getName().equalsIgnoreCase("table_name_which_is_an_enum_in_java")) {
        return;
    }
    super.printForeignKey(out, foreignKeyCounter, foreignKey, distributeForeignKey);
}

Another problem is the table where the foreign key is located in. A reference method and a transient field with another method are being created. This is just an example:

//class StammdatenZustand and Keys.FK_SPIELZEUG_STAMMDATEN_ZUSTAND do not exist anymore
@Override
@Nonnull
public List<ForeignKey<SpielzeugRecord, ?>> getReferences() {
    return Arrays.asList(Keys.FK_SPIELZEUG_STAMMDATEN_ZUSTAND);
}

private transient StammdatenZustand _stammdatenZustand; 

public StammdatenZustand stammdatenZustand() {
    if (_stammdatenZustand == null)
        _stammdatenZustand = new StammdatenZustand(this, Keys.FK_SPIELZEUG_STAMMDATEN_ZUSTAND);

    return _stammdatenZustand;
}

I dug into the source code of JavaGenerator and found the code causing the trouble, but I don't know how to find a workaround. The easiest solution would be to be able to modify the list that is being returned with tableDefinition.getForeignKeys(tableWhichWillBeAnEnum) and then call clear() on that, but sadly this time the list is not mutable.

EDIT: My current results are the following:

This is the table I want to convert into an enum (table name is "stammdaten_zustand")

enter image description here

with the following data

enter image description here

and this is the table where the pk is referenced (table name is "spielzeug")

enter image description here

My custom generator generates the following enum for the first table

public enum StammdatenZustandEnum {
    GUT("Das Produkt ist fast neuwertig.", 1, false),
    MITTEL("Einige Gebrauchsspuren sind vorhanden, die vermutlich einen Einfluss auf den Preis haben.", 2, true),
    SCHLECHT("Das Produkt ist in einem schlechten Zustand. Verkaufen wird sich kaum lohnen.", 3, true),
    UNBEKANNT("Zustand des Produkts ist nicht bekannt.", 4, true);

    private final String beschreibung;
    private final Integer testInt;
    private final boolean testBool;

    private StammdatenZustandEnum(String beschreibung,Integer testInt,boolean testBool) {
        this.beschreibung = beschreibung;
        this.testInt = testInt;
        this.testBool = testBool;
    }

    public String getBeschreibung() {
        return this.beschreibung;
    }

    public Integer getTestInt() {
        return this.testInt;
    }

    public boolean isTestBool() {
        return this.testBool;
    }

}

In the pom.xml of the project I also use a converter to map everything to the generated enum.

<forcedType>
    <userType>model.db.jooq.StammdatenZustandEnum</userType>
    <enumConverter>true</enumConverter>
    <includeExpression>.*\.Zustand_Kuerzel</includeExpression>
    <includeTypes>.*</includeTypes>
</forcedType>

And for example the generated POJO "Spielzeug" is mapped correctly.

public class Spielzeug implements Serializable {

    private static final long serialVersionUID = 1L;

    private Integer               produktId;
    private String                produktname;
    private Double                kaufpreis;
    private Boolean               verkaeuflich;
    private StammdatenZustandEnum zustandKuerzel; // mapped to enum
...

Which means I already do what you suggest if I understood you correctly?

However the generated table for "Spielzeug" still generates the following snippet

@Override
@Nonnull
public List<ForeignKey<SpielzeugRecord, ?>> getReferences() {
    return Arrays.asList(Keys.FK_SPIELZEUG_STAMMDATEN_ZUSTAND);
}

private transient StammdatenZustand _stammdatenZustand;

public StammdatenZustand stammdatenZustand() {
    if (_stammdatenZustand == null)
        _stammdatenZustand = new StammdatenZustand(this, Keys.FK_SPIELZEUG_STAMMDATEN_ZUSTAND);

    return _stammdatenZustand;
}

and the Keys class attempts to generate a foreign key

    public static final ForeignKey<SpielzeugRecord, StammdatenZustandRecord> FK_SPIELZEUG_STAMMDATEN_ZUSTAND = Internal.createForeignKey(Spielzeug.SPIELZEUG, DSL.name("FK_spielzeug_stammdaten_zustand"), new TableField[] { Spielzeug.SPIELZEUG.ZUSTAND_KUERZEL }, Keys.KEY_STAMMDATEN_ZUSTAND_PRIMARY, new TableField[] { StammdatenZustand.STAMMDATEN_ZUSTAND.KUERZEL }, true);

But both snippets throw errors because the "StammdatenZustand" POJO and Record do not exist.

On a side note: Generally speaking the mapping works and there are no erros when I just leave the generated Dao, POJO and Record. In this case I just have to tell the end user to basically ignore these classes and favour the generated enum.

1 Answers

Historic feature

Fun fact, there used to be a feature like this in jOOQ 1.x: #123

It was removed again in jOOQ 3.0, because it was overly complicated (as you've noticed yourself, via your own implementation attempt), and lead to numerous code generation bugs, all the while not answering real-world use-cases that wanted the feature to be even more powerful: #1740

To answer your specific question: Rather than trying to remove meta data about such lookup tables, why not just use a Converter on the foreign key and the primary key to reference the enum type rather than e.g. the Integer value. You can still keep the lookup tables in your generated code, but once you've replaced the type, you no longer have to join.

Of course, you can also still generate the enum from actual values in the lookup table.

Alternative approach using natural keys

But here's a "revolutionary" idea that helps you work around the issue very simply. How about defining a natural key instead, and reference that from all your foreign keys.

That way, you can still use a Converter on the column to bind the column to your enum type (just like I've described before), and thus forever avoid joins to the lookup table even in plain SQL queries, because the interesting lookup key is already referenced from all foreign keys. Note that this isn't against normalisation principles at all! It just removes useless surrogate keys.

For example:

instead of...

CREATE TABLE currency (
  id BIGINT NOT NULL PRIMARY KEY,
  iso_code CHAR(3) NOT NULL,
  description TEXT NOT NULL
);

CREATE TABLE transactions (
  id BIGINT NOT NULL PRIMARY KEY,
  amount NUMERIC NOT NULL,
  currency_id NOT NULL REFERENCES currency
)

...do this

CREATE TABLE currency (
  iso_code CHAR(3) NOT NULL PRIMARY KEY,
  description TEXT NOT NULL
);

CREATE TABLE transactions (
  id BIGINT NOT NULL PRIMARY KEY,
  amount NUMERIC NOT NULL,
  currency_iso_code NOT NULL REFERENCES currency
)

Here are some blog posts on the subject:

Related