I asked before, didn't get a great answer, but I've figured some stuff out since then, so here goes:
(Note that some of this stuff may be wrong, I'm not an EJB or Tapestry expert, I'm just trying to weld the two together. (pun intended)). Q: How can/should I use EJBs/JPA in Tapestry? A: Tapestry currently doesn't support EJB injection, however, there's a tynamo-jpa project that support injecting EntityManager objects in a fashion similar to tapestry-hibernate. You can lookup EJBs easily as needed using the EJB3.1 naming convention though. Between the two, you can actually produce a "best of both worlds" environment for using JEE6. That is, you can keep pretty light sessions mostly, but when you need to write data, you can do that via an EJB. Read-Mostly using tynamo-jpa: Tynamo-jpa, like tapestry-hibernate, works best with read-mostly data. The cycle is something like this: 1. Link to page with primary key of entity stored in context portion of URL, page expects entity object. 2. Tapestry makes a new EntityManager (EntityManagers are lightweight objects in JPA). 3. Entity fetched from either database or level 2 cache, passed to Page. 4. Page generates, entities turned back into primary keys, stored in context. 5. Entity Manager thrown away. If you need to write data, you can fetch something into memory, modify it, and then save it using @CommitAfter. Despite the dramatic language ("throwing stuff away"), the trade offs are complex because of the various caches built into JPA. Think of it as if the only thing that Tapestry needs to store in the session is the primary keys that it can then, via JPA, use to lookup objects via the cache. So for searching and displaying data, tynamo-jpa does the job. But there's an assumption there about what most of your data looks like. For something like a webstore, 90% of the data is read-only catalog data that should be cached across users. The read/write data is stuff like the shopping cart, and that's what needs to be stored in the session, not the catalog, and it doesn't need to be cached so much. Or consider GMail. When you login to GMail, it has to fetch all of your mailboxes, but your data doesn't intersect with anyone elses data. So caching your mailboxes doesn't make sense, and fetching all of your mail data again for each page may not make sense. What makes more sense is to fetch the data once, and then keep it around in the session. Using EJB session beans for read/write data: EJBs come in 3 flavors: Singleton, which means everyone gets the same EJB, Stateless, which means they're kept in pools and re-used, and Stateful which means "one per customer". Normally, the container manages the distinction for you. Since Tapestry doesn't directly support EJB injection though, though you can trust Singleton's to be singleton's, I'm not sure what happens with Stateless session beans. But that's ok, because in this case, we want a Stateful session bean, because we want an EntityManager that has data that persists across requests. This is what mine looks like: @Stateful public class SchemaEditEJB { //~ Instance fields ---------------------------------------------------------------------------- /** Information for EntityManager we want injected when EJB is built*/ @PersistenceContext( unitName = "LocalDB2GlassFish", type = PersistenceContextType.EXTENDED ) protected EntityManager em; //~ Methods ------------------------------------------------------------------------------------ /** * Static method to lookup our EJB using standardized name spaces * */ static SchemaEditEJB lookupSchemaEditEJB() { for (String prefix: new String[] { "", "java:module/", "java:app/MyApp/", "java:global/MyApp/" } ) { try { Context context = new InitialContext(); return (SchemaEditEJB) context.lookup(prefix + SchemaEditEJB.class.getSimpleName()); } catch (NamingException ex) { Logger.getLogger(SchemaEditEJB.class.getName()).log(Level.SEVERE, null, ex); } } return null; } /** * Return an entity manager */ public EntityManager getEntityManager() { return em; } /** * Persist an object * */ public void persist(Object o) { em.persist(o); } /** * revert an object * */ public void revert(Object o) { em.refresh(o); } /** * Save everything to the database, and throw the EJB away */ @Remove @TransactionAttribute(TransactionAttributeType.REQUIRED) public void commit() { em.commit(); } /** * Ignore changes, throw the EJB away */ @Remove public void uncommit() { } } // end class SchemaEditEJB This isn't much more than a wrapper around an EntityManager. The idea here is to use the EJB goodness to handle transactions, building the EntityManager, and holding the data between requests. I have a second object which is stored in the Tapestry SessionStore that lazily creates the EJB when its needed: /** * Lazily get EJB as needed */ public SchemaEditEJB getEJB() { if (null == ejb) { ejb = SchemaEditEJB.lookupSchemaEditEJB(); } return ejb; } /** * return EJB's entityManager */ public EntityManager getEntityManager() { return getEJB().getEntityManager(); } (Actually, I'm using a conversation pattern so multiple edits can be going on in parallel, but whatever) Gotchas: Since we're not using injection, the container can't throw away the EJB for us if we call commit/uncommit above. I have to do that manually when someone calls commit: if (ejb != null) { ejb.commit(); ejb = null; } Why this is the best of both worlds: 1. You can use tynamo-jpa for your read-mostly data where you want to fetch-display-toss information. For instance, in my case, I'm building a CRUD app, and part of the CRUD app is searching for items. Mostly those objects get used once and tossed, so a transient EntityManager is the way to go. (This isn't exactly a unique insight, this is precisely what Stateless EJBs are for. ) 2. For write-focused data like a shopping cart, or different cache requirements like Gmail, you can lookup a stateful EJB that you can use to hold an extended entity manager to function as L1 cache and session store. In my CRUD app, if you're creating or updating, I spawn a conversation with an attached EJB. Thoughts, comments, gotchas about EJB lifecycles I didn't consider? pierce