skapral skapral - 23 days ago 9
Java Question

How to manage PK generation with Cayenne 4.0 + PostgreSQL 9.4

I have:


  • PostgreSQL 9.4

  • Apache cayenne 4.0.M3

  • A schema which consists of one simplest table "proba":

    CREATE TABLE proba (
    id bigint NOT NULL,
    value character varying(255),
    CONSTRAINT proba_pkey PRIMARY KEY (id)
    )

  • A simple Main method:

    public static void main(String[] args) {
    ServerRuntime runtime = ServerRuntimeBuilder.builder()
    .addConfig("cayenne-project.xml")
    .build();

    ObjectContext ctx = runtime.newContext();

    CayenneDataObject newObject = new CayenneDataObject();
    newObject.writeProperty("value", "proba1");
    ctx.registerNewObject(newObject);
    ctx.commitChanges();
    }

  • A simple cayenne-project.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <domain project-version="7">
    <map name="datamap"/>
    <node name="datanode"
    factory="org.apache.cayenne.configuration.server.XMLPoolingDataSourceFactory"
    schema-update-strategy="org.apache.cayenne.access.dbsync.SkipSchemaUpdateStrategy">
    <map-ref name="datamap"/>
    <data-source>
    ....
    </data-source>
    </node>
    </domain>

  • A simple datamap.map.xml (hand-maded):

    <?xml version="1.0" encoding="utf-8"?>
    <data-map xmlns="http://cayenne.apache.org/schema/7/modelMap"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://cayenne.apache.org/schema/7/modelMap http://cayenne.apache.org/schema/7/modelMap.xsd"
    project-version="7">
    <property name="defaultPackage" value="ru.xxx"/>
    <property name="defaultSchema" value="public"/>
    <db-entity name="proba" schema="public">
    <db-attribute name="id" type="BIGINT" isPrimaryKey="true" isGenerated="false" length="19"/>
    <db-attribute name="value" type="VARCHAR" length="255"/>
    </db-entity>
    <obj-entity name="Proba" dbEntityName="proba">
    <obj-attribute name="value" type="java.lang.String" db-attribute-path="value"/>
    </obj-entity>
    </data-map>



Trying it, I got the following output:

INFO: --- transaction started.
Nov 15, 2016 5:06:26 PM org.apache.cayenne.log.CommonsJdbcEventLogger logQuery
INFO: SELECT nextval('public.pk_proba')
Exception in thread "main" org.apache.cayenne.CayenneRuntimeException: [v.4.0.M3 Feb 08 2016 16:38:05] Commit Exception
at org.apache.cayenne.access.DataContext.flushToParent(DataContext.java:776)
at org.apache.cayenne.access.DataContext.commitChanges(DataContext.java:693)
at com.echelon.proba.cayenne.Main.main(Main.java:27)
Caused by: org.postgresql.util.PSQLException: ERROR: relation "public.pk_proba" does not exist
Position: 16
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2458)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2158)


So, cayenne expects a sequence named pk_proba. Why? I did't mean it to be generated. I didn't mentioned any postgresql sequences neither in my schema, nor in the Cayenne mappings.

So, I have two questions:


  • How can I suppress Cayenne attempts to generate ids and make Cayenne to fail fast if no identity is provided for specific entity at commit time?

  • May I customize a way how Cayenne manages PK auto-generation in my project (preferable solution would be the one without involving the Cayenne Modeller)?


Answer

TL;DR: "pk_proba" is the default name of a sequence to use for PK generation. If you want Cayenne default PK mechanism to function, you need to provide special sequences in PostgreSQL.

A longer version. You need to provide a PK to each inserted object one way or another. Cayenne PK generation algorithm roughly works like this:

  • If PK provided by the user as an object property, use it.
  • If PK is propagated from a master object via relationship, use it.
  • If PK is an auto_increment column in DB, use it (supported on PG since 4.0.M4)
  • If everything else failed, use Cayenne PK generator.

The last strategy requires you to prepare DB objects. Cayenne uses different strategies depending on the target DB. For PostgreSQL it will be sequences. In the Modeler go to "Tools > Generate Database Schema" and uncheck all checkboxes except "Create Primary Key Support". Then use the generated SQL to update your DB.

Now if you indeed want Cayenne to fail at step 4 (why though? you do want your insert to succeed after all), you can use a custom PkGenerator. Here is how you can load this via dependency injection using a custom DI module:

class CustomAdapterFactory extends DefaultDbAdapterFactory {
    public CustomAdapterFactory(
       @Inject("cayenne.server.adapter_detectors") 
       List<DbAdapterDetector> detectors) {
        super(detectors);
    }

    @Override
    public DbAdapter createAdapter(
        DataNodeDescriptor nodeDescriptor, 
        DataSource dataSource) throws Exception {

        AutoAdapter adapter = 
           (AutoAdapter) super.createAdapter(nodeDescriptor, dataSource);

        // your PkGenerator goes here
        adapter.setPkGenerator(...);
        return adapter;
    }
}

// add this when creating ServerRuntime
Module module = new Module() {
        @Override
        public void configure(Binder binder) {

            binder.bind(DbAdapterFactory.class).to(CustomAdapterFactory.class);
        }
    };

Admittedly this can be made easier (and we are planning to expose PkGenerator as a DI service), but it should work. Just make sure this is really what you need.