Apache Cayenne > Index > Cayenne Examples > Cayenne In Motion
Introduction
This example is a linear approach to demonstrate some of the core features of Cayenne. It implements an exceptionally simple customer order schema as a command-line program (no web framework bits to get in the way of demonstrating how Cayenne works). It is designed to be stepped through using a debugger and the code is heavily commented. Also, it is packaged as an Eclipse project, but should be easy enough to use in another IDE.
Requirements
To run the example, you will need:
- A Cayenne-supported database (I used PostgreSQL).
- The JDBC driver for your database (and in your CLASSPATH).
- Cayenne 1.2 in your CLASSPATH (although most likely will work with 1.1).
- Java 1.4.x+ and Eclipse (or another IDE) and debugger (to step through code).
- The code: CayenneExample.tar.gz.
Of course, if you don't want to set everything up, you can just read through the tutorial. You can also download just the Main.java file if it'll help reading through it.
Schema
The schema for the database consists of four tables:

Each Customer can have many Orders. Each Order can have many LineItems. Each LineItem has a single reference Item. The Item table is "read mostly" and is populated at the beginning of the program run and then referenced afterwards. (Kind of like a US State table would be.)
Cayenne Modeler
Cayenne Modeler is a GUI-based tool for editing and viewing your model files. It is included with Cayenne. Make sure you download the OS-specific version of Cayenne. (Each version includes the same modeler, but for OS X and Windows, includes a double-clickable version for convenience.)
Run Cayenne Modeler and open the cayenne.xml file included in the example. You see a window similar to:

Play around with the model – I won't document it here since there is already documentation elsewhere, but for those just reading through, I'll describe a bit of it and include some screenshots.
In the top section, you can see the four Java versions of our tables (Customer, etc). The bottom four (Customers, etc. – I like naming my tables in the plural, since they store many records) are the database schema mappings. Beneath that are two pre-canned queries (FindOrder and UnfilledOrders) which can be reused throughout your code. Finally, beneath that, is a DataNode, which tells Cayenne how to connect to the DB.
Some other things to note are the Java class name Customer is mapped to, the table it is mapped to, and that optimistic locking is turned on.
Our Customer class doesn't have many attributes:

For this example, we only needed the first name and last name. Also note that both attributes are used for (optimistic) locking.
Clicking on the relationship tab shows us the order relationship:

You can see that a Customer has a too-many relationship called "orders" to Order. Also note that that the delete rule is cascade. If you delete a customer record, Cayenne will cascade delete all order records, too. Again, we are using optimistic locking even on the relationship.
It is also interesting to look at the Order record to see the relationships for it:

The order has a to-one relationship back to the customer, but has a no-action delete rule – deleting an order does NOT automatically delete a customer. However, deleting an order cascade deletes all line items associated with the order.
Queries can also be defined inside the modeler. This is useful when it is a common query which might be reused throughout your code. Here is a simple example of what a query in the modeler looks like:

You must give the query a name (FindOrder), an object to root the query at (Order), and a few other details. The qualifier is used to construct the WHERE clause when selecting from the database.
In order to run the example, you need to create the database schema. Cayenne can do this for you. You need to edit the information under "CayenneExampleDomainNode" to be appropriate for your database (mainly the JDBC information). When done, select "CayenneExampleDomainMap" and then generate the database schema using the Tools -> Generate Database Schema menu option. You'll see something like:

Assuming the connection information is correct, click Continue and you'll see:

For this example, it is important not to create FK support because of the way the database is cleaned out. Make sure everything is selected under the Tables tab and click Generate, which will connect to the database and create all the tables and PK support (it executes all the SQL shown in the text view).
Normally when using the modeler, you have to generate Java class files, too, but the example already has them generated for you. Take a look at Customer.java and _Customer.java (and the others) if you are curious.
With the database prepared, you can run the example.
Running the Example
The example is pretty much a top-to-bottom execution of Main.java, with a few side steps into the helper methods. It is best to run in the debugger, stepping through line-by-line to observe the output and the state of the variables changing. What is shown below is the execution and the output. The comments in the code will, for the most part, serve as the documentation, so please read them, too.
Download, uncompress, and import CayenneExample.tar.gz into Eclipse (or another IDE) and open up Main.java. Set a breakpoint in the main in the main program and then debug it:
public static void main(String[] args) { // Set a break point on the following line and then step into the // constructor. It creates the DataContext and initializes Cayenne. Main main = new Main();
public Main() { super(); // Create the Cayenne DataContext. This will also initialize the Cayenne framework. dataContext = DataContext.createDataContext();
DEBUG DefaultConfiguration: using domain file name: cayenne.xml
DEBUG DefaultConfiguration: canInitialize started.
DEBUG DefaultConfiguration: initialize starting.
DEBUG ResourceLocator: URL found with classloader:
file:/Users/mrg/Projects/eclipse/workspace/CayenneExample/bin/cayenne.xml
INFO RuntimeLoadDelegate: started configuration loading.
INFO RuntimeLoadDelegate: loaded domain: CayenneExampleDomain
DEBUG ResourceLocator: URL found with classloader:
file:/Users/mrg/Projects/eclipse/workspace/CayenneExample/bin/CayenneExampleDomainMap.map.xml
INFO RuntimeLoadDelegate:
loaded <map name='CayenneExampleDomainMap' location='CayenneExampleDomainMap.map.xml'>.
INFO RuntimeLoadDelegate:
loading <node name='CayenneExampleDomainNode' datasource='CayenneExampleDomainNode.driver.xml'
factory='org.objectstyle.cayenne.conf.DriverDataSourceFactory'>.
INFO RuntimeLoadDelegate: using factory: org.objectstyle.cayenne.conf.DriverDataSourceFactory
INFO DriverDataSourceFactory:
loading driver information from 'CayenneExampleDomainNode.driver.xml'.
DEBUG ResourceLocator: URL found with classloader:
file:/Users/mrg/Projects/eclipse/workspace/CayenneExample/bin/CayenneExampleDomainNode.driver.xml
INFO DriverDataSourceFactory: loading driver org.postgresql.Driver
INFO DriverDataSourceFactory: loading user name and password.
INFO QueryLogger: Created connection pool: jdbc:postgresql://localhost:5432/mrg
Driver class: org.postgresql.Driver
Min. connections in the pool: 1
Max. connections in the pool: 1
INFO RuntimeLoadDelegate: loaded datasource.
DEBUG ResourceLocator: URL found with classloader:
jar:file:/usr/local/java/cayenne/cayenne-1.2RC1/lib/cayenne.jar!/org/objectstyle/cayenne/dba/postgres/types.xml
INFO RuntimeLoadDelegate: loaded map-ref: CayenneExampleDomainMap.
DEBUG Configuration: added domain: CayenneExampleDomain
INFO RuntimeLoadDelegate: finished configuration loading in 2404 ms.
DEBUG DefaultConfiguration: initialize finished.
DEBUG DefaultConfiguration: didInitialize finished.
Cayenne logs where it found all of the configuration information. The logging is controlled through the log4j.properties file. Note that a database connection has NOT been established at this point.
// Cayenne supports sending "raw" SQL to the database via SQLTemplates. // We prepare the database by deleting any existing data. NOTE: If you // created your database with constraints, this will probably fail. The // QueryChain allows all of these deletes (or other operations) to be done // in a single transaction. QueryChain chain = new QueryChain(); // Add raw SQL templates to the chain. chain.addQuery(new SQLTemplate(Customer.class, "DELETE FROM Customers")); chain.addQuery(new SQLTemplate(Item.class, "DELETE FROM Items")); chain.addQuery(new SQLTemplate(LineItem.class, "DELETE FROM LineItems")); chain.addQuery(new SQLTemplate(Order.class, "DELETE FROM Orders")); // Execute all of the SQL statements. dataContext.performNonSelectingQuery(chain); }
INFO QueryLogger: --- will run 4 queries. INFO QueryLogger: Opening connection: jdbc:postgresql://localhost:5432/mrg Login: mrg Password: ******* INFO QueryLogger: +++ Connecting: SUCCESS. INFO QueryLogger: --- transaction started. INFO QueryLogger: DELETE FROM Customers INFO QueryLogger: === updated 4 rows. INFO QueryLogger: DELETE FROM Items INFO QueryLogger: === updated 11 rows. INFO QueryLogger: DELETE FROM LineItems INFO QueryLogger: === updated 4 rows. INFO QueryLogger: DELETE FROM Orders INFO QueryLogger: === updated 4 rows. INFO QueryLogger: +++ transaction committed.
Cayenne realized it had to connect to the database here before starting the transaction. Also, if you are running the example for the first time, you'll see 0 rows for each delete. (I have run it many times before and the database had records.)
// Step into this method, too, and step through the example.
main.runExample();
}
// Runs the example. Be sure you step into this or have a breakpoint set. private void runExample() { // Create all the items and customers (and customer orders). These two // initialize our example database with initial data. Step into both of // these! createItems();
// Helper method to create (insert) all of the "static" Items in the database. private void createItems() { Item item; // Cayenne needs to have database objects registered with a DataContext. // This allows Cayenne to track changes to the objects and to know // which objects to consider when it is time to generate SQL and // communicate changes with the database. The easist way to do this // is to create your objects in a DataContext right from the start. // Do this using createAndRegisterNewObject() and pass the .class // you want and Cayenne will create, register, and return the // object ready to use. item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("PowerBook12C"); item.setDescription("Apple PowerBook 12\" 1.5 GHz G4 with Combo Drive"); item.setPrice(new BigDecimal(1499.00)); // Notice that on the following line, we re-assign item. Don't worry, the // item created above won't be lost -- it'll still be in the DataContext // instance. item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("PowerBook12S"); item.setDescription("Apple PowerBook 12\" 1.5 GHz G4 with Super Drive"); item.setPrice(new BigDecimal(1699.00)); item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("PowerBook15C"); item.setDescription("Apple PowerBook 15\" 1.5 GHz G4 with Combo Drive"); item.setPrice(new BigDecimal(1999.00)); item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("PowerBook15S"); item.setDescription("Apple PowerBook 15\" 1.67 GHz G4 with Super Drive"); item.setPrice(new BigDecimal(2299.00)); item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("PowerBook17S"); item.setDescription("Apple PowerBook 17\" 1.67 GHz G4 with Super Drive"); item.setPrice(new BigDecimal(2699.00)); item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("iMac17C"); item.setDescription("Apple iMac 17\" 1.8 GHz G5 with Combo Drive"); item.setPrice(new BigDecimal(1299.00)); item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("iMac17S"); item.setDescription("Apple iMac 17\" 2.0 GHz G5 with Super Drive"); item.setPrice(new BigDecimal(1499.00)); item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("iMac20S"); item.setDescription("Apple iMac 20\" 2.0 GHz G5 with Super Drive"); item.setPrice(new BigDecimal(1799.00)); item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("PowerMac20D"); item.setDescription("Apple PowerMac 2.0 GHz Dual G5 with Super Drive"); item.setPrice(new BigDecimal(1999.00)); item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("PowerMac23D"); item.setDescription("Apple PowerMac 2.3 GHz Dual G5 with Super Drive"); item.setPrice(new BigDecimal(2499.00)); item = (Item) dataContext.createAndRegisterNewObject(Item.class); item.setName("PowerMac27D"); item.setDescription("Apple PowerMac 2.7 GHz Dual G5 with Super Drive"); item.setPrice(new BigDecimal(2999.00)); // Pay particular attention to the SQL logged by commitChanges(). When // you tell a Cayenne DataContext to commit it's changes, it looks for // all changes and then sends them to the database. For the following // line, it'll allocate primary keys for all the Items above and then // insert them into the database. Prior to commitChanges(), nothing // has been saved to the database. The above createAndRegisterNewObject() // method only created the objects in memory, but commitChanges() will // persist them. dataContext.commitChanges(); }
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT nextval('pk_items')
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: INSERT INTO Items (description, name, price, primaryKey) VALUES (?, ?, ?, ?)
INFO QueryLogger: [batch bind: 'Apple PowerMac 2.0 GHz Dual G5 with Super Drive', 'PowerMac20D', 1999, 560]
INFO QueryLogger: [batch bind: 'Apple iMac 17" 1.8 GHz G5 with Combo Drive', 'iMac17C', 1299, 561]
INFO QueryLogger: [batch bind: 'Apple PowerBook 15" 1.5 GHz G4 with Combo Drive', 'PowerBook15C', 1999, 562]
INFO QueryLogger: [batch bind: 'Apple PowerBook 17" 1.67 GHz G4 with Super Drive', 'PowerBook17S', 2699, 563]
INFO QueryLogger: [batch bind: 'Apple PowerMac 2.7 GHz Dual G5 with Super Drive', 'PowerMac27D', 2999, 564]
INFO QueryLogger: [batch bind: 'Apple PowerBook 12" 1.5 GHz G4 with Super Drive', 'PowerBook12S', 1699, 565]
INFO QueryLogger: [batch bind: 'Apple PowerBook 12" 1.5 GHz G4 with Combo Drive', 'PowerBook12C', 1499, 566]
INFO QueryLogger: [batch bind: 'Apple PowerBook 15" 1.67 GHz G4 with Super Drive', 'PowerBook15S', 2299, 567]
INFO QueryLogger: [batch bind: 'Apple iMac 17" 2.0 GHz G5 with Super Drive', 'iMac17S', 1499, 568]
INFO QueryLogger: [batch bind: 'Apple PowerMac 2.3 GHz Dual G5 with Super Drive', 'PowerMac23D', 2499, 569]
INFO QueryLogger: [batch bind: 'Apple iMac 20" 2.0 GHz G5 with Super Drive', 'iMac20S', 1799, 570]
INFO QueryLogger: === updated 11 rows.
INFO QueryLogger: +++ transaction committed.
Cayenne needs to insert Item records into the database, but doesn't yet have any primary keys cached (by default, it caches 20 of them at a time). Therefore it grabs and caches some keys (each database is different – PostgreSQL uses sequences). After it has the keys, it can insert the items. You can see in the above bind statements, the primary keys start at 560 for my database and keep incrementing. If you look carefully, you'll also notice the first item inserted is not the first item added to the DataContext. This is normal and nothing to worry about.
createCustomers();
// Helper method to create all of the example Customers in the database. // This is extremely similar to createItems() above. Read the comments // there if needed. private void createCustomers() { Customer customer; customer = (Customer) dataContext.createAndRegisterNewObject(Customer.class); customer.setFirstName("Jonathan"); customer.setLastName("Kent"); // Step into the createOrder() method. createOrder(customer, "iMac17C", 1);
// Helper method to create a Customer's order. private Order createOrder(Customer customer, String product, int quantity) { // Locate the item for the Customer. Step into findItem(). Item item = findItem(product);
// Helper method to find an Item by name. private Item findItem(String name) { // The following expression was made "final" because it'll never change, // which is just a small optimization. The expression looks for the // Item (the root object) of the passed in name. final Expression expression = Expression.fromString("name = $name"); Map parameters = new HashMap(1); parameters.put("name", name); SelectQuery query = new SelectQuery(Item.class, expression.expWithParameters(parameters)); List items = dataContext.performQuery(query);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.description, t0.name, t0.price, t0.primaryKey
FROM Items t0
WHERE t0.name = ? [bind: 'iMac17C'] - prepared in 45 ms.
INFO QueryLogger: === returned 1 row. - took 178 ms.
INFO QueryLogger: +++ transaction committed.
// Make sure the item came back and there was only one match. if (items.size() == 1) return (Item) items.get(0); logger.debug("Could not find item!"); return null; }
// If we found the item ... if (item != null) { // Create and register a new LineItem and Order. LineItem lineItem = (LineItem) dataContext.createAndRegisterNewObject(LineItem.class); Order order = (Order) dataContext.createAndRegisterNewObject(Order.class); // Add the order to the Customer's to-many "orders" relationship. Note // that this will automatically set the Order's relationship back to the // Customer. All of the Cayenne relationship methods do that. For a // to-many relationship, you "addTo" the relationship, for a to-one, you // use a "set" method. customer.addToOrders(order); // Add the line item to the Order's to-many "items" relationship. order.addToLineItems(lineItem); // Set the line item's to-one "item" relationship to the item. lineItem.setItem(item); // Set initial values for the order and line item. order.setIsFilled(false); order.setOrderID("Q" + counter++); lineItem.setQuantity(quantity); // Return the order (uncommitted to the database). return order; } // Problem -- couldn't find the item, return null. return null; }
customer = (Customer) dataContext.createAndRegisterNewObject(Customer.class);
customer.setFirstName("Martha");
customer.setLastName("Kent");
// If you stepped into the createOrder() call above, you can probably just
// step over the following ones.
createOrder(customer, "PowerBook12C", 1);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.description, t0.name, t0.price, t0.primaryKey
FROM Items t0
WHERE t0.name = ? [bind: 'PowerBook12C'] - prepared in 16 ms.
INFO QueryLogger: === returned 1 row. - took 30 ms.
INFO QueryLogger: +++ transaction committed.
customer = (Customer) dataContext.createAndRegisterNewObject(Customer.class);
customer.setFirstName("Clark");
customer.setLastName("Kent");
createOrder(customer, "PowerBook15S", 1);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.description, t0.name, t0.price, t0.primaryKey
FROM Items t0
WHERE t0.name = ? [bind: 'PowerBook15S']
INFO QueryLogger: === returned 1 row. - took 12 ms.
INFO QueryLogger: +++ transaction committed.
customer = (Customer) dataContext.createAndRegisterNewObject(Customer.class);
customer.setFirstName("Chloe");
customer.setLastName("Sullivan");
createOrder(customer, "PowerMac27D", 2);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.description, t0.name, t0.price, t0.primaryKey
FROM Items t0
WHERE t0.name = ? [bind: 'PowerMac27D']
INFO QueryLogger: === returned 1 row. - took 20 ms.
INFO QueryLogger: +++ transaction committed.
customer = (Customer) dataContext.createAndRegisterNewObject(Customer.class);
customer.setFirstName("Lex");
customer.setLastName("Luthor");
createOrder(customer, "PowerBook17S", 1);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.description, t0.name, t0.price, t0.primaryKey
FROM Items t0
WHERE t0.name = ? [bind: 'PowerBook17S']
INFO QueryLogger: === returned 1 row. - took 13 ms.
INFO QueryLogger: +++ transaction committed.
// Persist everything to the database. Watch the SQL logged and you'll // see primary keys fetched for the Customer, Order, and LineItem objects. // You'll also see inserts for all of these objects with the proper // primary and foreign keys set for everything. dataContext.commitChanges(); }
cayenne INFO QueryLogger: --- transaction started.
cayenne INFO QueryLogger: SELECT nextval('pk_customers')
cayenne INFO QueryLogger: SELECT nextval('pk_orders')
cayenne INFO QueryLogger: SELECT nextval('pk_lineitems')
cayenne INFO QueryLogger: --- will run 3 queries.
cayenne INFO QueryLogger: INSERT INTO Customers (firstName, lastName, primaryKey) VALUES (?, ?, ?)
cayenne INFO QueryLogger: [batch bind: 'Jonathan', 'Kent', 560]
cayenne INFO QueryLogger: [batch bind: 'Lex', 'Luthor', 561]
cayenne INFO QueryLogger: [batch bind: 'Clark', 'Kent', 562]
cayenne INFO QueryLogger: [batch bind: 'Martha', 'Kent', 563]
cayenne INFO QueryLogger: [batch bind: 'Chloe', 'Sullivan', 564]
cayenne INFO QueryLogger: === updated 5 rows.
cayenne INFO QueryLogger: INSERT INTO Orders (customerFK, isFilled, orderID, primaryKey) VALUES (?, ?, ?, ?)
cayenne INFO QueryLogger: [batch bind: 560, 'false', 'Q1000000', 560]
cayenne INFO QueryLogger: [batch bind: 564, 'false', 'Q1000003', 561]
cayenne INFO QueryLogger: [batch bind: 562, 'false', 'Q1000002', 562]
cayenne INFO QueryLogger: [batch bind: 563, 'false', 'Q1000001', 563]
cayenne INFO QueryLogger: [batch bind: 561, 'false', 'Q1000004', 564]
cayenne INFO QueryLogger: === updated 5 rows.
cayenne INFO QueryLogger: INSERT INTO LineItems (itemFK, orderFK, primaryKey, quantity) VALUES (?, ?, ?, ?)
cayenne INFO QueryLogger: [batch bind: 567, 562, 560, 1]
cayenne INFO QueryLogger: [batch bind: 566, 563, 561, 1]
cayenne INFO QueryLogger: [batch bind: 561, 560, 562, 1]
cayenne INFO QueryLogger: [batch bind: 563, 564, 563, 1]
cayenne INFO QueryLogger: [batch bind: 564, 561, 564, 2]
cayenne INFO QueryLogger: === updated 5 rows.
cayenne INFO QueryLogger: +++ transaction committed.
// Perform a variety of queries against the database and modify some of // the retrieved records, too. Step into the method. performQueries(); }
// This is the workhorse of our example for querying and retrieving data // from the database. Some of the records retrieved will be modified or // deleted, too. private void performQueries() { Expression expression; // Expressions contain WHERE criteria Map parameters; // Map of data to substitute in an expression SelectQuery query; // Query to use when fetching data List values; // Values returned from query // Create an expression (the "WHERE" clause, in SQL parlance) for the data // we want. expression = Expression.fromString("firstName = $firstName and lastName = $lastName"); // Expressions usually need to consume parameters (such as data entered // into a web application). We put that data in a HashMap. Variables // are substituted into the expression via the $ placeholders. The // above expression has two variables for substitution: $firstName and // $lastName. The keys in the HashMap need to match the variable names // (minus the $). parameters = new HashMap(2); parameters.put("lastName", "Kent"); // What about the firstName? Well, Cayenne will, by default, remove criteria // missing in the expression. Since we don't add a firstName to the HashMap, // Cayenne is smart enough to reduce the expression to "lastName = $lastName". // This can be very convenient in a search page/etc since the search will // automatically broaden when the user enters less information. // We can now create an SQL query with the expression and map data. The // expWithParameters() method returns a new expression (SelectQuery needs // an expression) built from the initial expression and substitution values. // The SelectQuery constructor below (there are other forms available) tells // Cayenne to root the query at the Customer object and to retrieve Customers // whose criteria matches the expression with the parameters in the HashMap. // When you step over the performQuery() call, Cayenne will access the database // and retrieve the matching Customer records and return them in a List. query = new SelectQuery(Customer.class, expression.expWithParameters(parameters)); values = dataContext.performQuery(query);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.firstName, t0.lastName, t0.primaryKey
FROM Customers t0
WHERE t0.lastName = ? [bind: 'Kent']
INFO QueryLogger: === returned 3 rows. - took 23 ms.
INFO QueryLogger: +++ transaction committed.
logger.info("Customers named Kent:"); logger.info(values); {noformat:bgColor=#DADAE2} INFO Main: Customers named Kent: INFO Main: [{<ObjectId:CustomerprimaryKey=560>; committed; [orders=>(..); firstName=>Jonathan; lastName=>Kent]}, {<ObjectId:CustomerprimaryKey=562>; committed; [orders=>(..); firstName=>Clark; lastName=>Kent]}, {<ObjectId:CustomerprimaryKey=563>; committed; [orders=>(..); firstName=>Martha; lastName=>Kent]}] {noformat} {code:java|title=performQueries() Method Continued} // Qualify the search a little more and add a firstName parameter. parameters.put("firstName", "Jonathan"); // Perform the query again and get new results. query = new SelectQuery(Customer.class, expression.expWithParameters(parameters)); values = dataContext.performQuery(query);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.firstName, t0.lastName, t0.primaryKey
FROM Customers t0
WHERE (t0.firstName = ?) AND
(t0.lastName = ?) [bind: 'Jonathan', 'Kent'] - prepared in 6 ms.
INFO QueryLogger: === returned 1 row. - took 15 ms.
INFO QueryLogger: +++ transaction committed.
logger.info("Customers named Jonathan Kent:");
logger.info(values);
INFO Main: Customers named Jonathan Kent:
INFO Main: [{<ObjectId:CustomerprimaryKey=560>; committed; [orders=>(..); firstName=>Jonathan; lastName=>Kent]}]
// Cayenne supports a dotted path notation for representing relationships. // The following expression, which will be rooted at the Customer, tells // Cayenne to also join with the Order, LineItem, and Item objects. The // information contained in the model is used to determine how to constuct // the joins. expression = Expression.fromString("orders.lineItems.item.name = $name"); parameters = new HashMap(1); parameters.put("name", "PowerMac27D"); // Pay particular attention to the SQL logged from the performQuery() here // to see how Cayenne created all the joins to retrieve the data. query = new SelectQuery(Customer.class, expression.expWithParameters(parameters)); values = dataContext.performQuery(query);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT DISTINCT t0.firstName, t0.lastName, t0.primaryKey
FROM Customers t0, Orders t1, LineItems t2, Items t3
WHERE t0.primaryKey = t1.customerFK AND
t1.primaryKey = t2.orderFK AND
t2.itemFK = t3.primaryKey AND
(t3.name = ?) [bind: 'PowerMac27D']
INFO QueryLogger: === returned 1 row. - took 21 ms.
INFO QueryLogger: +++ transaction committed.
logger.info("Customers who bought a PowerMac 2.7 GHz Dual G5:");
logger.info(values);
INFO Main: Customers who bought a PowerMac 2.7 GHz Dual G5:
INFO Main: [{<ObjectId:CustomerprimaryKey=564>; committed; [orders=>(..); firstName=>Chloe; lastName=>Sullivan]}]
// If you already have a database object in memory, Cayenne can handle // an expression referring to it, too. This is the same as the above // expression, except an entire complex Cayenne DataObject is the match // criteria. (The findItem() method will return the matching database // object -- step into it if you like.) expression = Expression.fromString("orders.lineItems.item = $item"); parameters = new HashMap(1); parameters.put("item", findItem("PowerBook17S"));
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.description, t0.name, t0.price, t0.primaryKey
FROM Items t0
WHERE t0.name = ? [bind: 'PowerBook17S']
INFO QueryLogger: === returned 1 row. - took 26 ms.
INFO QueryLogger: +++ transaction committed.
// Again, watch the SQL logged from performQuery(). Cayenne creates all the joins automatically. query = new SelectQuery(Customer.class, expression.expWithParameters(parameters)); values = dataContext.performQuery(query);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT DISTINCT t0.firstName, t0.lastName, t0.primaryKey
FROM Customers t0, Orders t1, LineItems t2
WHERE t0.primaryKey = t1.customerFK AND
t1.primaryKey = t2.orderFK AND
(t2.itemFK = ?) [bind: 563] - prepared in 32 ms.
INFO QueryLogger: === returned 1 row. - took 51 ms.
INFO QueryLogger: +++ transaction committed.
logger.info("Customers who bought a PowerBook 17:");
logger.info(values);
INFO Main: Customers who bought a PowerBook 17:
INFO Main: [{<ObjectId:CustomerprimaryKey=561>; committed; [orders=>(..); firstName=>Lex; lastName=>Luthor]}]
// This is another example of a complex query, doing joins in different // directions, and rooted at the LineItem instead of Customer. expression = Expression.fromString("item = $item and order.customer.firstName = $firstName and order.customer.lastName = $lastName"); parameters = new HashMap(3); parameters.put("item", findItem("PowerMac27D"));
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.description, t0.name, t0.price, t0.primaryKey
FROM Items t0
WHERE t0.name = ? [bind: 'PowerMac27D']
INFO QueryLogger: === returned 1 row. - took 12 ms.
INFO QueryLogger: +++ transaction committed.
parameters.put("firstName", "Chloe"); parameters.put("lastName", "Sullivan"); query = new SelectQuery(LineItem.class, expression.expWithParameters(parameters)); values = dataContext.performQuery(query);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.quantity, t0.itemFK, t0.orderFK, t0.primaryKey
FROM LineItems t0, Orders t1, Customers t2
WHERE t0.orderFK = t1.primaryKey AND
t1.customerFK = t2.primaryKey AND
((t0.itemFK = ?) AND
(t2.firstName = ?) AND
(t2.lastName = ?)) [bind: 564, 'Chloe', 'Sullivan'] - prepared in 28 ms.
INFO QueryLogger: === returned 1 row. - took 112 ms.
INFO QueryLogger: +++ transaction committed.
// It is usually a good idea to see how many rows you get back, especially // if you are only expecting a single row. if (values.size() == 1) { LineItem lineItem = (LineItem) values.get(0); // Update the quantity to 1 (instead of 2). Cayenne tracks changes to // your database objects. It knows not only which database objects have // been modified, but it also knows which attributes have been modified. lineItem.setQuantity(1); // When we commit the changes, since Cayenne is aware of all the changes, // it'll only update (or insert or delete) the database objects that have // changes. Also, if you are using optimistic locking (which is advisable // and this example is using it), Cayenne includes all of the original // values in the database object that you have specified to optimistically // lock on as part of the WHERE clause. Pay attention to the SQL logged // from commitChanges() to see how it does locking. You can also experiment // here and change the database contents for this record (say, to a 5) // before calling commitChanges() and Cayenne will throw an optimistic // locking exception. dataContext.commitChanges(); }
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: UPDATE LineItems
SET quantity = ?
WHERE primaryKey = ? AND quantity = ? AND itemFK = ? AND orderFK = ?
INFO QueryLogger: [bind: 1, 564, 2, 564, 561]
INFO QueryLogger: === updated 1 row.
INFO QueryLogger: +++ transaction committed.
// Find Jonathan Kent's record -- he called in and wants to cancel his order. expression = Expression.fromString("firstName = $firstName and lastName = $lastName"); parameters = new HashMap(2); parameters.put("firstName", "Jonathan"); parameters.put("lastName", "Kent"); query = new SelectQuery(Customer.class, expression.expWithParameters(parameters)); values = dataContext.performQuery(query);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.firstName, t0.lastName, t0.primaryKey
FROM Customers t0
WHERE (t0.firstName = ?) AND (t0.lastName = ?) [bind: 'Jonathan', 'Kent']
INFO QueryLogger: === returned 1 row. - took 15 ms.
INFO QueryLogger: +++ transaction committed.
if (values.size() == 1) { // Note in the logs that the following line must obtain primary keys // for the related objects since we have cascading delete turned on. // Deleting a customer deletes the associated orders and line items. // Yes, deleting one record can actually delete even more if you // model it to do that. This ensures that dependent records are // cleaned up automatically. dataContext.deleteObject((Customer) values.get(0)); // The log will show three deletes. dataContext.commitChanges(); }
INFO QueryLogger: --- will run 3 queries. INFO QueryLogger: --- transaction started. INFO QueryLogger: DELETE FROM LineItems WHERE primaryKey = ? AND quantity = ? AND itemFK = ? AND orderFK = ? INFO QueryLogger: [bind: 562, 1, 561, 560] INFO QueryLogger: === updated 1 row. INFO QueryLogger: DELETE FROM Orders WHERE primaryKey = ? AND isFilled = ? AND orderID = ? AND customerFK = ? INFO QueryLogger: [bind: 560, 'false', 'Q1000000', 560] INFO QueryLogger: === updated 1 row. INFO QueryLogger: DELETE FROM Customers WHERE primaryKey = ? AND firstName = ? AND lastName = ? INFO QueryLogger: [bind: 560, 'Jonathan', 'Kent'] INFO QueryLogger: === updated 1 row. INFO QueryLogger: +++ transaction committed.
// Cayenne allows expressions to be modeled in Cayenne Modeler, too. This // might be a convenient approach when an expression is commonly used in // many places in the code. It reduces the chance of errors and makes the // expression easier to maintain. These expression work just like the // above ones, though. Missing values can automatically be pruned out // when needed (although that isn't shown here). // As you can see here, a simple expression is pretty easy to call. The // "false" parameter tells Cayenne to return a cached value if possile // (that is, if "Result Caching" in the modeler isn't "No Result Caching" // and if the "Refresh Results" flag is not checked -- refresh results // will trump caching). values = dataContext.performQuery("UnfilledOrders", false);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.isFilled, t0.orderID, t0.customerFK, t0.primaryKey
FROM Orders t0
WHERE t0.isFilled = ? [bind: 0] - prepared in 64 ms.
INFO QueryLogger: === returned 4 rows. - took 97 ms.
INFO QueryLogger: +++ transaction committed.
logger.info("Unfilled Orders = " + values);
INFO Main: Unfilled Orders =
[{<ObjectId:OrderprimaryKey=561>; committed; [customer=>?; orderID=>Q1000003; lineItems=>(..); isFilled=>false]},
{<ObjectId:OrderprimaryKey=562>; committed; [customer=>?; orderID=>Q1000002; lineItems=>(..); isFilled=>false]},
{<ObjectId:OrderprimaryKey=563>; committed; [customer=>?; orderID=>Q1000001; lineItems=>(..); isFilled=>false]},
{<ObjectId:OrderprimaryKey=564>; committed; [customer=>?; orderID=>Q1000004; lineItems=>(..); isFilled=>false]}]
// One way to call with parameters. parameters = new HashMap(1); parameters.put("orderID", "Q1000003"); values = dataContext.performQuery("FindOrder", parameters, false);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.isFilled, t0.orderID, t0.customerFK, t0.primaryKey
FROM Orders t0
WHERE t0.orderID = ? [bind: 'Q1000003']
INFO QueryLogger: === returned 1 row. - took 14 ms.
INFO QueryLogger: +++ transaction committed.
logger.info("Order Q1000003 = " + values);
INFO Main: Order Q1000003 =
[{<ObjectId:OrderprimaryKey=561>; committed; [customer=>?; orderID=>Q1000003; lineItems=>(..); isFilled=>false]}]
// Another way to call with parameters. SelectQuery prototype = (SelectQuery) dataContext.getEntityResolver().lookupQuery("FindOrder"); parameters = new HashMap(1); parameters.put("orderID", "Q1000004"); query = prototype.queryWithParameters(parameters); values = dataContext.performQuery(query);
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: SELECT t0.isFilled, t0.orderID, t0.customerFK, t0.primaryKey
FROM Orders t0
WHERE t0.orderID = ? [bind: 'Q1000004']
INFO QueryLogger: === returned 1 row. - took 38 ms.
INFO QueryLogger: +++ transaction committed.
logger.info("Order Q1000004 = " + values);
INFO Main: Order Q1000004 =
[{<ObjectId:OrderprimaryKey=564>; committed; [customer=>?; orderID=>Q1000004; lineItems=>(..); isFilled=>false]}]
// That's all!
}
Optimistic Locking
Cayenne supports advanced optimistic locking. Using Cayenne Modeler, you can instruct Cayenne to optimistically lock on individual columns (typically all but BLOBs) and relationships. (See the screenshots of the modeler at the start of this page and you can see the optimistic locking checkboxes.) This gives you very fine-grained control and allows you to ensure that when an UPDATE is performed, the record is in the state Cayenne thinks the record should be in – it will prevent Cayenne from overwriting changes to the database made by an external process (batch job, SQL command line, etc).
So, how does this work in reality? Well, if we set a breakpoint on:
lineItem.setQuantity(1);
and then run the program, we will have an opportunity to change the quantity value to something Cayenne doesn't expect (the original value is 2). This will cause an optimistic locking exception and protect the data in the database. Skipping straight to the breakpoint, we can then run this SQL (the primary keys differ from above because the example has been run anew):
mrg=> select * from lineitems; itemfk | orderfk | primarykey | quantity --------+---------+------------+---------- 600 | 604 | 600 | 1 609 | 603 | 601 | 1 604 | 600 | 602 | 1 608 | 602 | 603 | 2 610 | 601 | 604 | 1 (5 rows) mrg=> update lineitems set quantity = 5 where primarykey = 603; UPDATE 1
We have now changed the database value to be inconsistent with the Cayenne state. Stepping over:
// Update the quantity to 1 (instead of 2). Cayenne tracks changes to // your database objects. It knows not only which database objects have // been modified, but it also knows which attributes have been modified. lineItem.setQuantity(1); // When we commit the changes, since Cayenne is aware of all the changes, // it'll only update (or insert or delete) the database objects that have // changes. Also, if you are using optimistic locking (which is advisable // and this example is using it), Cayenne includes all of the original // values in the database object that you have specified to optimistically // lock on as part of the WHERE clause. Pay attention to the SQL logged // from commitChanges() to see how it does locking. You can also experiment // here and change the database contents for this record (say, to a 5) // before calling commitChanges() and Cayenne will throw an optimistic // locking exception. dataContext.commitChanges();
gives this output:
INFO QueryLogger: --- will run 1 query.
INFO QueryLogger: --- transaction started.
INFO QueryLogger: UPDATE LineItems SET quantity = ? WHERE primaryKey = ? AND quantity = ? AND itemFK = ? AND orderFK = ?
INFO QueryLogger: [bind: 1, 603, 2, 608, 602]
INFO QueryLogger: *** error.
org.objectstyle.cayenne.access.OptimisticLockException: [v.1.2RC1 May 31 2006]
Optimistic Lock Failure, SQL:
[UPDATE LineItems SET quantity = ? WHERE primaryKey = ? AND quantity = ? AND itemFK = ? AND orderFK = ?],
WHERE clause bindings: [primaryKey=603, quantity=2, orderFK=602, itemFK=608]
at org.objectstyle.cayenne.access.jdbc.BatchAction.runAsIndividualQueries(BatchAction.java:235)
at org.objectstyle.cayenne.access.jdbc.BatchAction.performAction(BatchAction.java:117)
at org.objectstyle.cayenne.access.DataNodeQueryAction.runQuery(DataNodeQueryAction.java:95)
at org.objectstyle.cayenne.access.DataNode.performQueries(DataNode.java:309)
at org.objectstyle.cayenne.access.DataDomainFlushAction.runQueries(DataDomainFlushAction.java:255)
at org.objectstyle.cayenne.access.DataDomainFlushAction.flush(DataDomainFlushAction.java:177)
at org.objectstyle.cayenne.access.DataDomain.onSyncFlush(DataDomain.java:829)
at org.objectstyle.cayenne.access.DataDomain$2.transform(DataDomain.java:800)
at org.objectstyle.cayenne.access.DataDomain.runInTransaction(DataDomain.java:855)
at org.objectstyle.cayenne.access.DataDomain.onSync(DataDomain.java:797)
at org.objectstyle.cayenne.access.DataContext.flushToParent(DataContext.java:1261)
at org.objectstyle.cayenne.access.DataContext.commitChanges(DataContext.java:1165)
at com.foo.cayenne.example.Main.performQueries(Main.java:330)
at com.foo.cayenne.example.Main.runExample(Main.java:75)
at com.foo.cayenne.example.Main.main(Main.java:35)
You can see that Cayenne included "quantity = 2" as part of the WHERE clause. Although this example is simple, it should illustrate the power of optimistically locking on a table with 100 columns. Cayenne will include every column you chose to optimistically lock on and the original values when doing an UPDATE. This ensures that Cayenne cannot blindly overwrite changes made behind the scenes. How you handle this exception is application-specific. In a web application, you may choose to force the user to login again to get fresh data before continuing.