Blog

Tuesday, 28 February 2012

Seam3-persistence-framework comes to town

Seam2 had great "framework-in-framework" set of classes to speed up CRUD development. I'm talking about EntityHome and EntityQuery. If you miss that stuff a lot in Seam3, don't you worry, cause IT Crowd has migrated EntityHomes and such successfully to seam3 and we use it in production.
We've added some cool features:
  • dynamic parameters (if you don't want to expose your EntityQuery via EL)
  • EntityQueryDataModel (to make DB pagination for JSF dataTable a breaze)

Installation

All you need to do is attach this jar or add following Maven entry:
<dependency>
    <groupId>pl.com.it-crowd</groupId>
    <artifactId>seam3-persistence-framework</artifactId>
    <version>1.0.0</version>
</dependency>
Of course you need to add our artifactory to your settings.xml:
<repositories>
    <repository>
        <id>it-crowd.com.pl</id>
        <url>http://artifactory.it-crowd.com.pl/repo</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>
            
Now your're ready to extend EntityHome or EntityQuery.
public class UserHome extends EntityHome<User> {

}
            
Does it look familiar? It's identical as in Seam2!

Differences

We don't like that Seam2 bound EntityHome and EntityQuery too tightly to view layer. We usually used those components for more then just a CRUD. Actually we were re-using them wherever possible.

Scope & EL

So in Seam3 by default EntityHome and EntityQuery are in Dependant scope and don't have to be accessible via EL. If you want your i.e. UserHome to be accessible via EL and reside in Conversation scope you're free to do this:
                @Named
                @ConversationScoped
                public class UserHome extends EntityHome<User> {

                }
            

Return values

Previously EntityHome was returning String values from persist(), update(), remove() methods. It's because they were ment to be used in JSF navigation rules (I think). Because we now want to decouple our components from view layer those methods will return booleans.

Entity converter

There is no more s:converter but we've got another one:
                <h:selectOneListbox id="city" value="#{citySelector.selectedCity}" size="1" onchange="#{rich:element('hM')}.submit()">
                    <f:selectItem itemValue="#{null}" itemLabel="#{messages['CRUD.all']}"/>
                    <f:selectItems value="#{citySelector.availableCities}" var="city" itemValue="#{city}"
                        itemLabel="#{city.getName(localeSelector.selectedLocale.language)}"/>
                    <f:converter converterId="entityConverter"/>
                </h:selectOneListbox>
            
What is cool with this converter, it can handle both null values and transient objects (careful). But there is one requirement, your entity must implement pl.com.it_crowd.seam.framework.Identifiable interface:
                public interface Identifiable<T> {

                    public T getId();
                }
            
If your entity's getId() method returns null then it is treated as transient entity and during conversion from string to object a new instance will be created, so if you've written something into transient entity and convert it using EnityConverter then that info will be lost. So be careful, and don't you even think about synchronizing/waiting on such entity!

Re-use and decoupling

We've found it most usefull if EntityHome and EntityQuery are in DependantScope and are not available via EL. This is because sometimes we want to use them as DAO or business logic in different components that are used at the same time but each of those components set's up EntityHome or EntityQuery differently.
Let's see an example. We've got a page where we show a table with all events but at the top we also have a box where we show only future events. Both lists can easily be backed by 2 instances of the same EntityQuery. Let's see code abstract.
Below is backing bean for the main list.
                @Named
                @ViewScoped
                public class AllEventsView {
                // ------------------------------ FIELDS ------------------------------

                    @Inject
                    @Archive
                    private EventList eventList;

                // --------------------- GETTER / SETTER METHODS ---------------------

                    public EventList getEventList()
                    {
                        return eventList;
                    }
                }
            
Here we've got a backing bean for future events box, a Request scoped bean available via EL as futureEvents.
                @Model
                public class FutureEvents {
                // ------------------------------ FIELDS ------------------------------

                    @Inject
                    @Future
                    private EventList eventList;

                // --------------------- GETTER / SETTER METHODS ---------------------

                    public EventList getEventList()
                    {
                        return eventList;
                    }

                }
            
We also need a producer for our lists.
                public class EventListProducer {
                // ------------------------------ FIELDS ------------------------------

                    @Inject
                    private EventList eventList;

                // -------------------------- OTHER METHODS --------------------------

                    @Produces
                    @Archive
                    public EventList getArchive()
                    {
                        return eventList;
                    }

                    @Produces
                    @NextBets
                    public EventList getNextBets()
                    {
                        eventList.getSearchCriteria().setFutureOnly(true);
                        return eventList;
                    }
                }
            
And finally our EventList.
                public class EventList extends EntityQuery <Event> implements Serializable {
                // ------------------------------ FIELDS ------------------------------

                    private static final String DATE_SORT_FIELD = "e.date";

                    private SearchCriteria searchCriteria = new SearchCriteria();

                // --------------------------- CONSTRUCTORS ---------------------------

                    public EventList()
                    {
                        setEjbql("select e from Event e");
                        AbstractCondition nextBetsCondition = new FreeCondition("e.date>=",
                searchCriteria.futureOnlyBridge);
                        setConditions(Arrays.<AbstractCondition>asList(nextBetsCondition));
                        toggleSort(DATE_SORT_FIELD);
                    }

                // --------------------- GETTER / SETTER METHODS ---------------------

                    public SearchCriteria getSearchCriteria()
                    {
                        return searchCriteria;
                    }


                // -------------------------- INNER CLASSES --------------------------

                    public class SearchCriteria {
                // ------------------------------ FIELDS ------------------------------

                        private boolean futureOnly;

                        private DynamicParameter futureOnlyBridge = new DynamicParameter() {
                            @Override
                            public Object getValue()
                            {
                                return futureOnly ? new Date() : null;
                            }
                        };


                        public boolean isFutureOnly()
                        {
                            return futureOnly;
                        }

                        public void setFutureOnly(boolean futureOnly)
                        {
                            this.futureOnly = futureOnly;
                        }
                    }
                }
            
Notice that our EventList doesn't have any scope annotatnios nor is it @Named. We just inject it into view backing beans like FutureEvents and AllEventsView. We could of course put it into Conversation or View scope and make it @Named but then we would have the same instance of component injected into FutureEvents and AllEventsView and we don't want such situation. We could alternatively create a producer that would produce @Named EventList which is fine, but from my experience it's better to introduce the view layer which needs to tweak the EventList up before exposing it to facelets.

EntityQueryDataModel

How do you implement DB pagination? It's out-of-the-box for RichFaces components. Just instead of #{bean.eventList.resultList} bind #{bean.eventList.dataModel} to value attribute of your rich component and set "rows" attribute. EntityQueryDataModel will now load only 20 items from DB per page and rich:dataScroller takes care of switching pages back and forth.
                <rich:dataTable id="lE" value="#{allEventsView.eventList.dataModel}" var="event" rows="20">
                    <rich:column>
                        <f:facet name="header">ID</f:facet>
                        #{event.id}
                    </rich:column>
                    <rich:column>
                        <f:facet name="header">name</f:facet>
                        <h:link value="#{event.name}" outcome="/view/event/eventDetails.xhtml">
                            <f:param name="id" value="#{event.id}"/>
                        </h:link>
                    </rich:column>
                    <f:facet name="footer">
                        <rich:dataScroller/>
                    </f:facet>
                </rich:dataTable>
            

Dynamic parameters without EL

In Seam2 we used to add restrictions which contained EL expressions. It was enough to change value of bean's attribute that was targeted via such EL expression to have the EntityQuery refreshed results for new criteria. We found it that usually we were using the EntityQuery alone as such backing bean. Because the EntityQuery was exposed to EL it was no problem to use it's name in restrictions. But problems arised when we wanted to have two EntityQueries of the same type in single page with different criteria. I know we could solve such issue with @Role but we would then end up with very many roles, probably one for each view.
Our solution was to make EntityQuery a dependant bean, not exposed to EL. But this way we couldn't use it in EL restrictions. To resolve this we've created Conditions.
You've seen them earlier in this article. Let's look at it once again.
                    public EventList()
                    {
                        setEjbql("select e from Event e");
                        AbstractCondition nextBetsCondition = new FreeCondition("e.date>=", searchCriteria.futureOnlyBridge);
                        setConditions(Arrays.<AbstractCondition>asList(nextBetsCondition));
                        toggleSort(DATE_SORT_FIELD);
                    }
            
We've got several conditions:
  • FreeCondition - included into query if all arguments are not null
  • AndCondition - included into query if any of arguments (sub-conditions) is not null, joins arguments with logical AND
  • OrCondition - included into query if any of arguments (sub-conditions) is not null, joins arguments with logical OR
  • IsNullCondition - always included into query, used to tell if argument is null
  • NotNullCondition - always included into query, used to tell if argument is not null
FreeCondition is most commonly used by us. It accepts any number of arguments which if not null are concatenated. Those arguments can be other conditions, objects or DynamicParameters. Regular objects are toString()'ed (null safe), conditions are evaluated and if they want to be rendered they are. DynamicParameters on the other hand are different. They are recalculated each time we call getResultList() on EntityQuery.
In our example above (EventList constructor) the searchCriteria.futureOnlyBridge is a dynamic parameter. It's called bridge (by us) because it usually just delegates to another method. In our example it checks value of futureOnly (boolean) attribute and if it is true it returns current date. Otherwise it returns null. So if we set futureOnly to true the nextBetsCondition will be included into query; If on the other hand set futureOnly to false, then futureOnlyBridge will return null and thus one of FreeCondition arguments will be null and it will not be included into query.
As we've said before FreeCondition (actually Free-,And- and OrCondition) accept any number of arguments (IsNull and NotNullCondition also accept many arguments, but will use only first one).
                new FreeCondition("(e.date>=", searchCriteria.futureOnlyBridge," or true=",isAdminBridge,")");
            
The condition above will render either
  • (e.date>=:qel0 or true=:qel1) - if both bridges return not null values (:qel0 is a query param that will have set value of searchCriteria.futureOnlyBridge; similarily :qel1 stands for isAdminBridge)
  • null - if any dynamic parameter is null

Community feedback needed

If you find this library usefull, find bugs, have comments, suggestions or questions please tell us and vote for SEAMPERSIST-59.