Chapter 3. Learning Cayenne API

Table of Contents

Getting started with ObjectContext
Getting started with persistent objects
Selecting Objects
Deleting Objects

Getting started with ObjectContext

In this section we'll write a simple main class to run our application, and get a brief introduction to Cayenne ObjectContext.

Creating the Main Class

  • In IDEA create a new class called "Main" in the "org.example.cayenne" package.

  • Create a standard "main" method to make it a runnable class:

    package org.example.cayenne;
    
    public class Main {
    
        public static void main(String[] args) {
    
        }
    }
  • The first thing you need to be able to access the database is to create a ServerRuntime object (which is essentially a wrapper around Cayenne stack) and use it to obtain an instance of an ObjectContext.

    package org.example.cayenne;
    
    import org.apache.cayenne.ObjectContext;
    import org.apache.cayenne.configuration.server.ServerRuntime;
    
    public class Main {
    
        public static void main(String[] args) {
            ServerRuntime cayenneRuntime = ServerRuntime.builder()
                            .addConfig("cayenne-project.xml")
                            .build();
            ObjectContext context = cayenneRuntime.newContext();
        }
    }

    ObjectContext is an isolated "session" in Cayenne that provides all needed API to work with data. ObjectContext has methods to execute queries and manage persistent objects. We'll discuss them in the following sections. When the first ObjectContext is created in the application, Cayenne loads XML mapping files and creates a shared access stack that is later reused by other ObjectContexts.

Running Application

Let's check what happens when you run the application. But before we do that we need to add another dependency to the pom.xml - Apache Derby, our embedded database engine. The following piece of XML needs to be added to the <dependencies>...</dependencies> section, where we already have Cayenne jars:

<dependency>
   <groupId>org.apache.derby</groupId>
   <artifactId>derby</artifactId>
   <version>10.13.1.1</version>
</dependency>

Now we are ready to run. Right click the "Main" class in IDEA and select "Run 'Main.main()'".

In the console you'll see output similar to this, indicating that Cayenne stack has been started:

INFO: Loading XML configuration resource from file:/.../cayenne-project.xml
INFO: Loading XML DataMap resource from file:/.../datamap.map.xml
INFO: loading user name and password.
INFO: Connecting to 'jdbc:derby:memory:testdb;create=true' as 'null'
INFO: +++ Connecting: SUCCESS.
INFO: setting DataNode 'datanode' as default, used by all unlinked DataMaps

How to Configure Cayenne Logging

Follow the instructions in the logging chapter to tweak verbosity of the logging output.

Getting started with persistent objects

In this chapter we'll learn about persistent objects, how to customize them and how to create and save them in DB.

Inspecting and Customizing Persistent Objects

Persistent classes in Cayenne implement a DataObject interface. If you inspect any of the classes generated earlier in this tutorial (e.g. org.example.cayenne.persistent.Artist), you'll see that it extends a class with the name that starts with underscore (org.example.cayenne.persistent.auto._Artist), which in turn extends from org.apache.cayenne.CayenneDataObject. Splitting each persistent class into user-customizable subclass (Xyz) and a generated superclass (_Xyz) is a useful technique to avoid overwriting the custom code when refreshing classes from the mapping model.

Let's for instance add a utility method to the Artist class that sets Artist date of birth, taking a string argument for the date. It will be preserved even if the model changes later:

package org.example.cayenne.persistent;
            
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

import org.example.cayenne.persistent.auto._Artist;

public class Artist extends _Artist {

    static final String DEFAULT_DATE_FORMAT = "yyyyMMdd";

    /**
     * Sets date of birth using a string in format yyyyMMdd.
     */
    public void setDateOfBirthString(String yearMonthDay) {
        if (yearMonthDay == null) {
            setDateOfBirth(null);
        } else {

            LocalDate date;
            try {
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT);
                date = LocalDate.parse(yearMonthDay, formatter);
            } catch (DateTimeParseException e) {
                throw new IllegalArgumentException(
                        "A date argument must be in format '"
                        + DEFAULT_DATE_FORMAT + "': " + yearMonthDay);
            }

            setDateOfBirth(date);
        }
    }
}

Create New Objects

Now we'll create a bunch of objects and save them to the database. An object is created and registered with ObjectContext using "newObject" method. Objects must be registered with DataContext to be persisted and to allow setting relationships with other objects. Add this code to the "main" method of the Main class:

Artist picasso = context.newObject(Artist.class);
picasso.setName("Pablo Picasso");
picasso.setDateOfBirthString("18811025");

Note that at this point "picasso" object is only stored in memory and is not saved in the database. Let's continue by adding a Metropolitan Museum "Gallery" object and a few Picasso "Paintings":

Gallery metropolitan = context.newObject(Gallery.class);
metropolitan.setName("Metropolitan Museum of Art"); 

Painting girl = context.newObject(Painting.class);
girl.setName("Girl Reading at a Table");
        
Painting stein = context.newObject(Painting.class);
stein.setName("Gertrude Stein");

Now we can link the objects together, establishing relationships. Note that in each case below relationships are automatically established in both directions (e.g. picasso.addToPaintings(girl) has exactly the same effect as girl.setToArtist(picasso)).

picasso.addToPaintings(girl);
picasso.addToPaintings(stein);
        
girl.setGallery(metropolitan);
stein.setGallery(metropolitan);

Now lets save all five new objects, in a single method call:

context.commitChanges();

Now you can run the application again as described in the previous chapter. The new output will show a few actual DB operations:

org.apache.cayenne.configuration.XMLDataChannelDescriptorLoader load
INFO: Loading XML configuration resource from file:cayenne-project.xml
...
INFO: Connecting to 'jdbc:derby:memory:testdb;create=true' as 'null'
INFO: +++ Connecting: SUCCESS.
INFO: setting DataNode 'datanode' as default, used by all unlinked DataMaps
INFO: Detected and installed adapter: org.apache.cayenne.dba.derby.DerbyAdapter
INFO: --- transaction started.
INFO: No schema detected, will create mapped tables
INFO: CREATE TABLE GALLERY (ID INTEGER NOT NULL, NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: CREATE TABLE ARTIST (DATE_OF_BIRTH DATE, ID INTEGER NOT NULL, NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: CREATE TABLE PAINTING (ARTIST_ID INTEGER, GALLERY_ID INTEGER, ID INTEGER NOT NULL, 
      NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: ALTER TABLE PAINTING ADD FOREIGN KEY (ARTIST_ID) REFERENCES ARTIST (ID)
INFO: ALTER TABLE PAINTING ADD FOREIGN KEY (GALLERY_ID) REFERENCES GALLERY (ID)
INFO: CREATE TABLE AUTO_PK_SUPPORT (  
      TABLE_NAME CHAR(100) NOT NULL,  NEXT_ID BIGINT NOT NULL,  PRIMARY KEY(TABLE_NAME))
INFO: DELETE FROM AUTO_PK_SUPPORT WHERE TABLE_NAME IN ('ARTIST', 'GALLERY', 'PAINTING')
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('ARTIST', 200)
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('GALLERY', 200)
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('PAINTING', 200)
INFO: SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = ? FOR UPDATE [bind: 1:'ARTIST']
INFO: SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = ? FOR UPDATE [bind: 1:'GALLERY']
INFO: SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = ? FOR UPDATE [bind: 1:'PAINTING']
INFO: INSERT INTO GALLERY (ID, NAME) VALUES (?, ?)
INFO: [batch bind: 1->ID:200, 2->NAME:'Metropolitan Museum of Art']
INFO: === updated 1 row.
INFO: INSERT INTO ARTIST (DATE_OF_BIRTH, ID, NAME) VALUES (?, ?, ?)
INFO: [batch bind: 1->DATE_OF_BIRTH:'1881-10-25 00:00:00.0', 2->ID:200, 3->NAME:'Pablo Picasso']
INFO: === updated 1 row.
INFO: INSERT INTO PAINTING (ARTIST_ID, GALLERY_ID, ID, NAME) VALUES (?, ?, ?, ?)
INFO: [batch bind: 1->ARTIST_ID:200, 2->GALLERY_ID:200, 3->ID:200, 4->NAME:'Gertrude Stein']
INFO: [batch bind: 1->ARTIST_ID:200, 2->GALLERY_ID:200, 3->ID:201, 4->NAME:'Girl Reading at a Table']
INFO: === updated 2 rows.
INFO: +++ transaction committed.

So first Cayenne creates the needed tables (remember, we used "CreateIfNoSchemaStrategy"). Then it runs a number of inserts, generating primary keys on the fly. Not bad for just a few lines of code.

Selecting Objects

This chapter shows how to select objects from the database using ObjectSelect query.

Introducing ObjectSelect

It was shown before how to persist new objects. Cayenne queries are used to access already saved objects. The primary query type used for selecting objects is ObjectSelect. It can be mapped in CayenneModeler or created via the API. We'll use the latter approach in this section. We don't have too much data in the database yet, but we can still demonstrate the main principles below.

  • Select all paintings (the code, and the log output it generates):

List<Painting> paintings1 =  ObjectSelect.query(Painting.class).select(context); 
INFO: SELECT t0.GALLERY_ID, t0.ARTIST_ID, t0.NAME, t0.ID FROM PAINTING t0
INFO: === returned 2 rows. - took 18 ms.
  • Select paintings that start with "gi", ignoring case:

List<Painting> paintings2 = ObjectSelect.query(Painting.class)
        .where(Painting.NAME.likeIgnoreCase("gi%")).select(context); 
INFO: SELECT t0.GALLERY_ID, t0.NAME, t0.ARTIST_ID, t0.ID FROM PAINTING t0 WHERE UPPER(t0.NAME) LIKE UPPER(?)
      [bind: 1->NAME:'gi%'] - prepared in 6 ms.
INFO: === returned 1 row. - took 18 ms.
  • Select all paintings done by artists who were born more than a 100 years ago:

List<Painting> paintings3 = ObjectSelect.query(Painting.class)
        .where(Painting.ARTIST.dot(Artist.DATE_OF_BIRTH).lt(LocalDate.of(1900,1,1)))
        .select(context); 
INFO: SELECT t0.GALLERY_ID, t0.NAME, t0.ARTIST_ID, t0.ID FROM PAINTING t0 JOIN ARTIST t1 ON (t0.ARTIST_ID = t1.ID)
      WHERE t1.DATE_OF_BIRTH < ? [bind: 1->DATE_OF_BIRTH:'1911-01-01 00:00:00.493'] - prepared in 7 ms.
INFO: === returned 2 rows. - took 25 ms.

Deleting Objects

This chapter explains how to model relationship delete rules and how to delete individual objects as well as sets of objects. Also demonstrated the use of Cayenne class to run a query.

Setting Up Delete Rules

Before we discuss the API for object deletion, lets go back to CayenneModeler and set up some delete rules. Doing this is optional but will simplify correct handling of the objects related to deleted objects.

In the Modeler go to "Artist" ObjEntity, "Relationships" tab and select "Cascade" for the "paintings" relationship delete rule:

Repeat this step for other relationships:

  • For Gallery set "paintings" relationship to be "Nullify", as a painting can exist without being displayed in a gallery.

  • For Painting set both relationships rules to "Nullify".

Now save the mapping.

Deleting Objects

While deleting objects is possible via SQL, qualifying a delete on one or more IDs, a more common way in Cayenne (or ORM in general) is to get a hold of the object first, and then delete it via the context. Let's use utility class Cayenne to find an artist:

Artist picasso = ObjectSelect.query(Artist.class)
            .where(Artist.NAME.eq("Pablo Picasso")).selectOne(context);

Now let's delete the artist:

if (picasso != null) {
    context.deleteObject(picasso);
    context.commitChanges();
}

Since we set up "Cascade" delete rule for the Artist.paintings relationships, Cayenne will automatically delete all paintings of this artist. So when your run the app you'll see this output:

INFO: SELECT t0.DATE_OF_BIRTH, t0.NAME, t0.ID FROM ARTIST t0
      WHERE t0.NAME = ? [bind: 1->NAME:'Pablo Picasso'] - prepared in 6 ms.
INFO: === returned 1 row. - took 18 ms.
INFO: +++ transaction committed.
INFO: --- transaction started.
INFO: DELETE FROM PAINTING WHERE ID = ?
INFO: [batch bind: 1->ID:200]
INFO: [batch bind: 1->ID:201]
INFO: === updated 2 rows.
INFO: DELETE FROM ARTIST WHERE ID = ?
INFO: [batch bind: 1->ID:200]
INFO: === updated 1 row.
INFO: +++ transaction committed.