Table of Contents
In this section we'll write a simple main class to run our application, and get a brief introduction to Cayenne ObjectContext.
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.
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
Follow the instructions in the logging chapter to tweak verbosity of the logging output.
In this chapter we'll learn about persistent objects, how to customize them and how to create and save them in DB.
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); } } }
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.
This chapter shows how to select objects from the database using ObjectSelect
query.
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.
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.
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.
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.