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



Reply via email to