Search This Blog

Thursday 28 June 2012

Data Filters in Hibernate

I think everybody has at some point come across views in Database.Views generally represent a subset of the data contained in a table. A common scenario is one wherein we use soft deletion . For example instead of actually deleting an entity, we have a flag column in the row which we set as true indicating the entity has been deleted. We then create a view to access all the "alive" records.
Views thus provide us a filtered access of data in the table.(This is only one of its uses.) Hibernate provides something similar called dynamic views.
Consider a Shelf and Books on it. Some of the books have been marked as damaged/under repair. These books should not be available to certain modules of the application. The java classes are as below:
public class Shelf {
    private Integer id;
    private String code;    
    private Set<Book> filteredBooks = new HashSet<Book>();
        //setters - getters
}
public class Book {
    private String name;
    private Integer id;
    private Shelf shelf;
    private boolean underRepair;
}
As can be seen, the book class has a property "underRepair" which indicates if the book is, well under repair. We would like to restrict such books from access to certain modules like Library module or Sale module.
This can be achieved using data filters. let us look at the mapping for the book class:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.data.filter">
    <class name="Book" table="BOOK">
        <id name="id" type="integer">
            <column name="ID" />
            <generator class="native" />
        </id>
        <property name="name" type="string">
            <column name="Name" length="50" not-null="true" />
        </property>
        <property name="underRepair" type="boolean">
            <column name="UNDER_REPAIR" not-null="true" />
        </property>
        <many-to-one name="shelf" class="Shelf" foreign-key="BOOK_FK1">
            <column name="shelf_id"></column>
        </many-to-one>

        <filter name="booksWithRepairStatus"
            condition=" under_repair = :UnderRepairParam">
        </filter>
    </class>        
    <filter-def name="booksWithRepairStatus" >
        <!-- The filter accepts one runtime argument -->
        <filter-param name="UnderRepairParam" type="boolean" />
    </filter-def>
</hibernate-mapping>
The hbm is more or less same as what we have created earlier. However two points of note - One is the <filter-def> element. This is where we define the filter. As can be seen it is not inside the class element but only within the <hibernate-mapping>. The filters are all defined at a global scope and hence must have a globally unique name. Our filter has been defined to accept one parameter with name UnderRepairParam of type boolean.
The other new element of the <filter> element. This is defined inside the Book class and is applied to instances of Book class.
The filter condition is the actual condition that gets added to the  SQL where clause. In this case we have added the restriction on the Under_Repair column. As the content is SQL we can also use native SQL operators or functions here. Now to actually use the filter created.
public static void testHQlOnFilters() {
    final Session session = sessionFactory.openSession();

    final List<Book> allbooks = session.createQuery("from Book").list();
    int i = 0;
    System.out.println("All the books are : ");
    for (Book book : allbooks) {
        System.out.println(  (++i) + " : " + "Name : " + book.getName() + " ,id " + book.getId() );
    }
    session.close();
    
    final Session filteredSession = sessionFactory.openSession();
    final Filter filter = filteredSession.enableFilter("booksWithRepairStatus");
    filter.setParameter("UnderRepairParam", true);
    filter.validate();
    
    final List<Book> goodBooks = filteredSession.createQuery("from Book").list();
    i = 0;
    System.out.println("All the filtered books are : ");
    for (Book book : goodBooks) {
        System.out.println(  (++i) + " : " + "Name : " + book.getName() + " ,id " + book.getId() );
    }
    //Also works for criteria objects
    filteredSession.close();
}
The first session simply executes a select * on the Books table. In the second session, however we have enabled the filter on the session. The value of the parameter has then been applied. The filter has then been validated. The method call is optional and it throws a Hibernate exception if the filter parameters have not been set. The session is now ready to apply the filter on Book entities.
The logs indicate the same:
765  [main] INFO  org.hibernate.cfg.HbmBinder  - Mapping class: com.data.filter.
Book -> BOOK
...
937  [main] DEBUG org.hibernate.cfg.HbmBinder  - Applying filter [booksWithRepai
rStatus] as [ under_repair = :UnderRepairParam]
937  [main] DEBUG org.hibernate.cfg.HbmBinder  - Parsing filter-def [booksWithRe
pairStatus]
937  [main] DEBUG org.hibernate.cfg.HbmBinder  - adding filter parameter : Under
RepairParam -> boolean
937  [main] DEBUG org.hibernate.cfg.HbmBinder  - parameter heuristic type : org.
hibernate.type.BooleanType@a8c488
937  [main] DEBUG org.hibernate.cfg.HbmBinder  - Parsed filter-def [booksWithRep
airStatus]
...
3156 [main] DEBUG org.hibernate.cfg.Configuration  - Preparing to build session 
factory with filters : {booksWithRepairStatus=org.hibernate.engine.FilterDefinit
ion@ce0c5}
...
3937 [main] INFO  org.hibernate.impl.SessionFactoryImpl  - building session fact
ory
3937 [main] DEBUG org.hibernate.impl.SessionFactoryImpl  - Session factory 
constructed with filter configurations : {booksWithRepairStatus=org.hibernate.engine.
FilterDefinition@ce0c5}
...
    select
        book0_.ID as ID0_,
        book0_.Name as Name0_,
        book0_.UNDER_REPAIR as UNDER3_0_,
        book0_.shelf_id as shelf4_0_ 
    from
        BOOK book0_
...
All the books are : 
1 : Name : Lord Of The Rings ,id 1
2 : Name : To Fly ,id 2
3 : Name : Lakers ,id 3
...
5234 [main] DEBUG org.hibernate.hql.ast.AST  - --- SQL AST ---
 \-[SELECT] QueryNode:'SELECT' querySpaces (BOOK)
    +-[SELECT_CLAUSE] SelectClause:'{derived select clause}'
    |  +-[SELECT_EXPR] SelectExpressionImpl: 'book0_.ID as ID0_' {FromElement{ex
plicit,not a collection join,not a fetch join,fetch non-lazy properties,classAli
as=null,role=null,tableName=BOOK,tableAlias=book0_,origin=null,colums={,classNam
e=com.data.filter.Book}}}
    |  \-[SQL_TOKEN] SqlFragment: 'book0_.Name as Name0_, book0_.UNDER_REPAIR as
 UNDER3_0_, book0_.shelf_id as shelf4_0_'
    +-[FROM] FromClause: 'from' FromClause{level=1, fromElementCounter=1, fromEl
ements=1, fromElementByClassAlias=[], fromElementByTableAlias=[book0_], fromElem
entsByPath=[], collectionJoinFromElementsByPath=[], impliedElements=[]}
    |  \-[FROM_FRAGMENT] FromElement: 'BOOK book0_' FromElement{explicit,not a c
ollection join,not a fetch join,fetch non-lazy properties,classAlias=null,role=n
ull,tableName=BOOK,tableAlias=book0_,origin=null,colums={,className=com.data.fil
ter.Book}}
    \-[WHERE] SqlNode: 'WHERE'
       \-[FILTERS] SqlNode: '{filter conditions}'
          \-[SQL_TOKEN] SqlFragment: ' book0_.under_repair = :booksWithRepairSta
tus.UnderRepairParam'
...
    select
        book0_.ID as ID0_,
        book0_.Name as Name0_,
        book0_.UNDER_REPAIR as UNDER3_0_,
        book0_.shelf_id as shelf4_0_ 
    from
        BOOK book0_ 
    where
        book0_.under_repair = ?
...
5250 [main] DEBUG org.hibernate.type.BooleanType  - binding 'true' to parameter:
 1
...
All the filtered books are : 
1 : Name : Lord Of The Rings ,id 1
2 : Name : Lakers ,id 3
As can be seen the second query returned fewer books than the first one. All HQL and Criteria queries executed on the Book class via the second session will apply the filter on it.
The data filter created does not apply to the load and get method. (However and HQL query of the form "from Book where id = 1" will return null if the row with id 1 fails the filter condition.) Also if I were to get a shelf entity and call shelf.getFilteredBooks() then all the books would be returned. The filter in this case is not applied on the collection.
It is possible to modify the Shelf class to ensure that the filter is applied to the books collection. The change is introduced in the mapping file for Shelf.
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.data.filter">
    <class name="Shelf" table="SHELF">
        <id name="id" type="integer">
            <column name="ID" />
            <generator class="native" />
        </id>
        <property name="code" type="string">
            <column name="CODE" length="50" not-null="true" />
        </property>
        <set name="filteredBooks" cascade="save-update" inverse="true">
            <key column="SHELF_ID" not-null="true" />
            <one-to-many class="Book" />
            <filter name="booksWithRepairStatus"
                condition=" under_repair = :UnderRepairParam">
            </filter>        
        </set>
        
    </class>
</hibernate-mapping>
As can be seen a filter element has been added to the set element. If were to navigate the relation now:
public static void testFilteredEntity() {
    final Session filteredSession = sessionFactory.openSession();
    final Filter filter = filteredSession.enableFilter("booksWithRepairStatus");
    filter.setParameter("UnderRepairParam", true);
    filter.validate();
        
    final Shelf shelf = (Shelf) filteredSession.load(Shelf.class, 1);
    final Set<Book> books = shelf.getFilteredBooks();
    int i = 0;
    for (Book book : books) {
        System.out.println(  (++i) + " : " + "Name : " + book.getName() + " ,id " + book.getId() );
    }    
}
The logs indicate that only two books were retrieved:
2578 [main] DEBUG org.hibernate.event.def.DefaultInitializeCollectionEventListen
er  - disregarding cached version (if any) of collection due to enabled filters 
2578 [main] DEBUG org.hibernate.SQL  - 
    select
        filteredbo0_.SHELF_ID as SHELF4_1_,
        filteredbo0_.ID as ID1_,
        filteredbo0_.ID as ID0_0_,
        filteredbo0_.Name as Name0_0_,
        filteredbo0_.UNDER_REPAIR as UNDER3_0_0_,
        filteredbo0_.shelf_id as shelf4_0_0_ 
    from
        BOOK filteredbo0_ 
    where
        filteredbo0_.under_repair = ? 
        and filteredbo0_.SHELF_ID=?
2656 [main] DEBUG org.hibernate.event.def.DefaultInitializeCollectionEventListen
er  - collection initialized
1 : Name : Lord Of The Rings ,id 1
2 : Name : Lakers ,id 3
The above filter will work only if the filter has been enabled on the session. Else it will ignore the filter settings and fire a normal select query.
The logs also indicate that filtered collections do not work well with the cache. This is something that I will investigate later :)

No comments:

Post a Comment