Tuesday, November 22, 2011

CDI Query Module First Alpha Released!


It's been some months now since we started exploring CDI extensions as a small exercise. As it turned out, the exercise forged into something usable which we're pushing now in the open as a first Alpha shot.

So I'm happy to announce the availability of the CDI Query Module on Maven Central! The module helps you creating JPA queries with much less boilerplate, and is of course leveraging the CDI extension API as well as Seam Solder. Some of the feature highlights are:

Query By Name

Assuming you have a Person entity which looks probably similar to this:

@Entity
public class Person { 

    ... // primary key etc. skipped

    @Getter @Setter
    private String firstName;
    
    @Getter @Setter
    private String lastName;

}

You can simply create a DAO interface to query for Persons:

@Dao
public interface PersonDao extends EntityDao<Person, Long> {

    Person findByFirstNameAndLastName(String firstName, String lastName);

}

This interface does not need to be implemented. A client can just inject it, call the interface method and in the background, the JPA query is automatically created and executed. To create the query, the method name is analyzed and matching the name to entity properties.

public class PersonAction {

    @Inject
    private PersonDao personDao;
    
    public void lookup() {
        person = personDao.findByFirstNameAndLastName(firstName, lastName);
    }

}

Note that the base interface contains also a couple of other methods which you might also expect from an entity DAO. Ideally, you should not need to inject an EntityManager anymore.

Query by Query Strings and Named Queries

Of course matching property names is not extremely safe to refactorings (some more validation support here is on the roadmap) - if you like to have more control over your JPA queries, you can also annotate the method with the query to execute:

@Dao
public interface PersonDao extends EntityDao<Person, Long> {

    @Query("select p from Person p where p.ssn = ?1")
    Person findBySSN(String ssn);

    @Query(named=Person.BY_FULL_NAME)
    Person findByFullName(String firstName, String lastName);

}

Criteria API Simplifications

If you're not a big fan of query strings but rather prefer using the JPA 2 criteria API, we also allow to simplify this with a small utility API:

public abstract class PersonDao extends AbstractEntityDao<Person, Long> {

    public List<Person> findAdultFamilyMembers(String name, Integer minAge) {
        return criteria()
                    .like(Person_.name, "%" + name + "%")
                    .gtOrEq(Person_.age, minAge)
                    .eq(Person_.validated, Boolean.TRUE)
                    .orderDesc(Person_.age)
                    .createQuery()
                    .getResultList();
    }

}

All the code is hosted and documented on GitHub. Please:

  • Give feedback! If you find this useful or actually not so, we're happy to hear what is still missing.
  • Participate! Forking and creating pull requests are really a breeze on GitHub :-)


Credits:

  • Bartek Majsak for improving the initial code, taking care about quality reports and soon finalizing the stuff on the validation branch ;-) (just kidding, check out Bartek's cool work on the Arquillian Persistence Module!)
  • Grails GORM for inspiring me for this Java implementation
  • The CDI folks for a really great specification
  • Last but not least the Arquillian guys, developing and testing this stuff is pure fun with Arquillian!

24 comments:

ror said...

It's like Spring Data JPA http://www.springsource.org/spring-data/jpa

rmannibucau said...

Yes it is like spring data or openejb dynamic proxy feature but i love to have it in CDI.

Good job!

George Gastaldi said...

Excellent ! I´ll love to see it in Seam Persistence or Apache CODI

Thomas said...

Oh yes, forgot the credits to Spring Data JPA formerly aka Hades!

I definitely also prefer the CDI way - standards based and portable. Getting this under the umbrella of a bigger project is of course the long term goal, for now we're trying to get some input to make it prime time ready.

Tavi said...

Hallelujah! :)

Looks nice and I think it's immensly useful to reduce the omnipresent code noise in 'enterprise' apps!

You might want to take a look at http://projectlombok.org/ in case you don't know it already...

Regards!

Josh Long said...

WOw! It's an (incomplete) copy of Spring Data JPA. And no credit's given? That's pretty low.

Thomas said...

Nah, credits already given in my comment above. I guess the idea of proxying interfaces to generate queries is around much longer than both GORM and Spring Data JPA.

abdelgadir said...

great work - many thanks.
I have a few suggestion for improvement based on my current requirements.
The first set of suggestions is to add a few more methods to the EntityDao interface. below is how they are currently implemented in my project:

public T createAndFlushAndRefresh(final T entity) {
if (entity != null) {
getEntityManager().persist(entity);
getEntityManager().flush();
getEntityManager().refresh(entity);
}
return entity;
}


public void deleteByPrimaryKey(final Class type, final Serializable pk) {
if (pk != null) {
Object entity = getEntityManager().find(type, pk);
if (entity != null) {
delete(entity);
}
}
}



public void delete(final Object entity) {
if (entity != null) {
EntityManager em = getEntityManager();
em.remove(em.contains(entity) ? entity : em.merge(entity));
}
}

to rename the findBy(PK) method to findByPK(PK)

public boolean exists(final Class type, final Serializable primaryKey) {
//return false for a null primary key
return primaryKey == null ? false : (findByPrimaryKey(type, primaryKey) != null);
}


/**
* This is a special version of count() that finds how many instances of 'root' will be returned given the
* specified restriction.
*/
private Long count(final Root root, final Predicate restriction, final CriteriaBuilder builder) {
//TODO exclude inactive records from the count
CriteriaQuery countQuery = builder.createQuery(Number.class);
countQuery.select(builder.count(root));
countQuery.where(restriction);
return getEntityManager().createQuery(countQuery).getSingleResult().longValue();
}

abdelgadir said...

Continue,

The second more important set of suggestions is related to support for paging and for fetching certain fields as part of query (cleaner alternative to all the open-session-in-view stuff which is a solution to the dreaded lazyinitializationexception). currently, I have something like:

public SearchResultList findAll(final Class type, final @Nullable YPage paging,
final @Nullable YFetch fetch) {
CriteriaBuilder builder = getEntityManager().getCriteriaBuilder();
CriteriaQuery cq = builder.createQuery(type);
Root root = cq.from(type);
cq.select(root);
return executeQuery(cq, paging, fetch, builder, root);
}

/**
* execute query with given properties
*
* @param select root query
* @param paging paging
* @param criteriaBuilder root builder
* @param from parent from
* @return executed list
*/
private SearchResultList executeQuery(CriteriaQuery select, YPage paging, YFetch fetch,
CriteriaBuilder criteriaBuilder, Root from) {
if (paging == null) {
TypedQuery typedQuery = getEntityManager().createQuery(select);
applyFetch(fetch, typedQuery);
return new SearchResultList(typedQuery.getResultList());
}

//do the total count before setting the limits so as to work out the total retrievable.
//note that setting the limit may affect the restrictions e.g., by appending the equivalent of
//'where from.id >= paging.getFirstResultIndex'
long totalCount = count(from, select.getRestriction(), criteriaBuilder);

setOrders(select, paging, criteriaBuilder, from, new HashMap());
TypedQuery typedQuery = getEntityManager().createQuery(select);
setLimits(typedQuery, paging);
applyFetch(fetch, typedQuery);
return new SearchResultList(typedQuery.getResultList(), totalCount, paging);
}

/**
* set Orders of given criteria.
*
* @param select query select
* @param paging query page definition
* @param criteriaBuilder criteria builder
* @param from root from
* @param joins joins map
*/
private void setOrders(CriteriaQuery select, YPage paging, CriteriaBuilder criteriaBuilder, Root from,
Map joins) {
if (paging == null) {
return;
}
if (paging.isOrdered()) {
ArrayList jpaOrders = new ArrayList();
List orders = paging.getOrders();
//to set the jpa orders, we first need to join on the objects whose properties we are using to order by
//e.g., if we have 'order by employers.name', we will first need to join to employers if not already joined
for (YOrder daoOrder : orders) {
Path path = extractPath(from, joins, daoOrder.getName());
Order jpaOrder = (daoOrder.getSort().equals(YOrder.Sort.ASC)) ? criteriaBuilder.asc(path)
: criteriaBuilder.desc(path);
jpaOrders.add(jpaOrder);
}
select.orderBy(jpaOrders.toArray(new Order[0]));
}
}

abdelgadir said...

private Path extractPath(Root from, Map joins, String property) {
Path path;
String[] strings = DaoUtils.splitDot(property);

From join = getLatestJoin(from, joins, strings);
path = join.get(strings[strings.length - 1]);

return path;
}

/**
* create latest join from given root criteria from
*
* @param from root
* @param joins join map cache
* @param strings '.' splitted field array
* @return get latest join before property
*/
private From getLatestJoin(Root from, Map joins, String[] strings) {
// /not implemented yet
From join = from;
for (int j = 0; j < strings.length - 1; j++) {
String propertyName = strings[j];
String key = keyMaker(strings, j);

if (joins.containsKey(key)) {
join = joins.get(key);
} else {
if (j == 0) {
join = from.join(propertyName);
} else {
join = join.join(propertyName);
}
joins.put(key, join);
}
}
return join;
}

/**
* given a property path, construct a (sub)path ending at index j. Uses '.' as the path separator
*
* @param strings property path
* @param j index of last property
* @return constructed (sub)path
*/
private String keyMaker(String[] strings, int j) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i <= j; i++) {
String string = strings[i];
if (i > 0) {
stringBuilder.append(".");
}
stringBuilder.append(string);
}
return stringBuilder.toString();
}

/**
* set limits for query
*
* @param query query
* @param limit page limits
*/
private void setLimits(Query query, YLimit limit) {
if (limit == null) {
return;
}
if (limit.isPaged()) {
query.setFirstResult(limit.getFirstResultIndex());
}
if (limit.isPageSized()) {
query.setMaxResults(limit.getPageSize());
}
}

private void applyFetch(YFetch fetch, Query query){
persistenceJPAFetchStrategy.applyFetch(fetch, query);
}


//we would have one for each provider e.g., hibernate etc
@Alternative
public class OpenJpaFetchStrategy implements IPersistenceJPAFetchStrategy {

public void applyFetch(YFetch fetch, Query query) {
if (fetch != null) {
OpenJPAQuery openJPAQuery = OpenJPAPersistence.cast(query);
FetchPlan fetchPlan = openJPAQuery.getFetchPlan();
fetchPlan.setMaxFetchDepth(fetch.getMaxFetchDepth());
Map> fetchProps = fetch.getFetchProperties();
for (Class clazz : fetchProps.keySet()) {
Collection fieldNames = fetchProps.get(clazz);
fetchPlan.addFields(clazz, fieldNames);
}
}
}
}

abdelgadir said...

the YLimit, YPage, and YOrder are inspired and based on YDAO opensource project see google code). I have extracted and adapted some of their work (and fixed a couple bugs) for my purposes so credit to them.

/**
* Created by IntelliJ IDEA.
* User: age
* Date: 18/11/11
* Time: 23:51
* To change this template use File | Settings | File Templates.
*
* Copyright (C) 2010 altuure http://www.altuure.com/projects/yagdao
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package hospital.model.dao;

import java.io.Serializable;


/**
* DAO meta class for paging and sorting.
*
* @author altuure
*/
public class YLimit implements Serializable {
private static final long serialVersionUID = 1L;
/**
* not assigned value.
*/
protected static final int NOT_ASSIGNED = -1;
/**
* index of first result.
*/
protected int firstResultIndex = NOT_ASSIGNED;
/**
* result set page size. The page size specifies the maximum number of records to retrieve
*/
protected int pageSize = NOT_ASSIGNED;

/**
* limit result with given properties.
*
* @param firstResultIndex first result index
* @param pageSize page size
*/
public YLimit(int firstResultIndex, int pageSize) {
this.firstResultIndex = firstResultIndex;
this.pageSize = pageSize;
}

/**
* unlimited result size from given index.
*
* @param firstResultIndex first result index
*/
public YLimit(int firstResultIndex) {
this(firstResultIndex, NOT_ASSIGNED);
}

/**
* default cons.
*/
public YLimit() {
this(NOT_ASSIGNED, NOT_ASSIGNED);
}

/**
* first result index.
*
* @return first result
*/
public int getFirstResultIndex() {
return firstResultIndex;
}

/**
* @return page size.
*/
public int getPageSize() {
return pageSize;
}

/**
* get whether this limit object represents a page as opposed to the total results.
*
* @return whether firstResultIndex>0
*/
public boolean isPaged() {
return firstResultIndex >= 0;
}

/**
* get whether the page size is defined.
*
* @return pageSize>0
*/
public boolean isPageSized() {
return pageSize >= 0;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

YLimit yLimit = (YLimit) o;

if (firstResultIndex != yLimit.firstResultIndex) return false;
if (pageSize != yLimit.pageSize) return false;

return true;
}

@Override
public int hashCode() {
int result = firstResultIndex;
result = 31 * result + pageSize;
return result;
}

/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "YLimit [firstResultIndex=" + firstResultIndex + ", pageSize=" + pageSize + "]";
}
}

abdelgadir said...

package hospital.model.dao;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

public class YPage extends YLimit implements Serializable {
/**
* serialization key.
*/
private static final long serialVersionUID = 1L;
/**
* order sort list.
*/
private List orders = new ArrayList(2);

/**
* dao page serialization constructor.
*/
public YPage() {
super();
}

/**
* dao page serialization constructor.
*
* @param limit limits
*/
public static YPage copy(YLimit limit) {
if (limit == null) {
return new YPage();
} else {
return new YPage(limit.firstResultIndex, limit.pageSize);
}
}

/**
* order by given property asc. The property name is a path expression starting from
* the returned root entity excluding the root itself. For example, assuming property 'Employee.user.firstName',
* where Employee is the returned root class, it should be specified as 'user.firstName'.
*
* @param orderBy property asc.
*/
public YPage(String orderBy) {
this(orderBy, true, NOT_ASSIGNED, NOT_ASSIGNED);
}

/**
* order by given property. The orderBy property name is a path expression starting from
* the returned root entity excluding the root itself. For example, assuming property 'Employee.user.firstName',
* where Employee is the returned root class, it should be specified as 'user.firstName'.
*
* @param orderBy property .
* @param ascending is asc
*/
public YPage(String orderBy, boolean ascending) {
this(orderBy, ascending, NOT_ASSIGNED, NOT_ASSIGNED);
}

/**
* dao page. The orderBy property name is a path expression starting from
* the returned root entity excluding the root itself. For example, assuming property 'Employee.user.firstName',
* where Employee is the returned root class, it should be specified as 'user.firstName'.
*
* @param orderBy order property
* @param ascending asc
* @param firstResultIndex first result index
* @param pageSize result size
*/
public YPage(String orderBy, boolean ascending, int firstResultIndex, int pageSize) {
super(firstResultIndex, pageSize);
if (orderBy != null) {
orders.add(new YOrder(orderBy, (ascending) ? YOrder.Sort.ASC : YOrder.Sort.DESC));
}
}

/**
* limit result with given properties.
*
* @param firstResultIndex first result index
* @param pageSize page size
*/
public YPage(int firstResultIndex, int pageSize) {
this(null, false, firstResultIndex, pageSize);
}

/**
* unlimited result size from given index.
*
* @param firstResultIndex first result index
*/
public YPage(int firstResultIndex) {
this(null, false, firstResultIndex, NOT_ASSIGNED);
}

/**
* is ordered.
*
* @return is ordered
*/
public boolean isOrdered() {
return orders != null && !orders.isEmpty();
}

abdelgadir said...

/**
* util method to add order.The orderBy property name is a path expression starting from
* the returned root entity excluding the root itself. For example, assuming property 'Employee.user.firstName',
* where Employee is the returned root class, it should be specified as 'user.firstName'.
*
* @param orderBy order property.
* @param ascending sort order.
* @return this
*/
public YPage addOrder(String orderBy, boolean ascending) {
orders.add(new YOrder(orderBy, (ascending) ? YOrder.Sort.ASC : YOrder.Sort.DESC));
return this;
}

/**
* builder method to sort result set. The orderBy property name is a path expression starting from
* the returned root entity excluding the root itself. For example, assuming property 'Employee.user.firstName',
* where Employee is the returned root class, it should be specified as 'user.firstName'.
*
* @param orderBy propery to sort
* @param sort sort order
* @return this
*/
public YPage addOrder(String orderBy, YOrder.Sort sort) {
orders.add(new YOrder(orderBy, sort));
return this;
}

/**
* list of orders.
*
* @return orders
*/
public List getOrders() {
return orders;
}

/**
* orders to set.
*
* @param orders orders
*/
public void setOrders(List orders) {
this.orders = orders;
}

@Override
public int hashCode() {
int result = super.hashCode();
final int prime = 31;
result = prime * result + ((orders == null) ? 0 : orders.hashCode());

return result;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof YPage)) return false;
if (!super.equals(o)) return false;

YPage yPage = (YPage) o;

if (orders != null ? !orders.equals(yPage.orders) : yPage.orders != null) return false;

return true;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("YPage [orders=");
builder.append(orders);
builder.append(", YLimit=");
builder.append(super.toString());
builder.append("]");
return builder.toString();
}
}

abdelgadir said...

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;

import java.io.Serializable;

/**
* Class used to specify a sorted property
*
* @author altuure
* @author abdelgadir ibrahim
*/
public class YOrder implements Serializable {
private static final long serialVersionUID = 1L;

/**
* Available sort orders
*/
public enum Sort {
ASC, DESC
}

/**
* sort field name
*/
private String name;

/**
* sort order
*/
private Sort sort;

/**
* construct a YOrder to sort given property in asc order. The property name is a path expression starting from
* the returned root entity excluding the root itself. For example, assuming property 'Employee.user.firstName',
* where Employee is the returned root class, it should be specified as 'user.firstName'.
*
* @param name sort property name. Assumes asc sort order
*/
public YOrder(String name) {
this(name, Sort.ASC);
}

/**
* construct a YOrder to sort given property by the given sort order. The property name is a path expression
* starting from the returned root entity excluding the root itself. For example, assuming property
* 'Employee.user.firstName', where Employee is the returned root class, it should be specified as 'user.firstName'.
*
* @param name sort property
* @param sort sort order
*/
public YOrder(String name, Sort sort) {
super();
Preconditions.checkNotNull(Strings.emptyToNull(name), "sort property must not be null or empty");
this.name = name;
this.sort = sort;
}

/**
* get the sort property name
* @return sort property name
*/
public String getName() {
return name;
}

/**
* get sort order
*
* @return sort order
*/
public Sort getSort() {
return sort;
}

@Override
public String toString() {
return "YOrder{" +
"name='" + name + '\'' +
", sort=" + sort +
'}';
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((sort == null) ? 0 : sort.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
YOrder other = (YOrder) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (sort != other.sort)
return false;
return true;
}
}

abdelgadir said...

and last but not least



package hospital.model.dao;

import com.google.common.base.Preconditions;
import com.google.common.collect.ForwardingList;

import java.io.Serializable;
import java.util.Collections;
import java.util.List;

/**
* List encapsulating a result set with details of total result size and paging details.
*/
public class SearchResultList extends ForwardingList implements Serializable {
/**
* Paging details. A search result could be paged in the sense that it represents a single page of the total
* retrievable results. In such case, the paging variable describes which page is being returned.
* The paging index is zero-based.
*/
private YPage paging;
/**
* total number of results that would have been returned if no maxResults had been specified.
* In other words, it is the total retrievable result size; not just for a single page but for all possible pages.
* This total count may or may not match the number of actual results being returned by this search result instance.
*/
private long totalCount;
/**
* decorated list containing the results being returned. This list doesn't necessarily represent the total
* retrievable results, for example, when the results are being returned in junks/pages
*/
private List delegate;

public SearchResultList() {
this(Collections.emptyList());
}

public SearchResultList(List delegate) {
this(delegate, null);
}

/**
* non-paged search result list i.e., the results being returned by this instance represent all retrievable results.
* In this case the totalCount==result.size()
*/
public SearchResultList(List result, YLimit limit) {
this(result, result==null? 0 : result.size(), YPage.copy(limit));
}

/**
* paged search result list i.e., the set of results being returned by this instance represent the content of only
* one page. Total count is the total retrievable across all pages.
* In other words, 'the results in this instance (which are stored in delegate) represent only one page
* (as defined by paging) of the total set of retrievable results which is equals to totalCount'.
*/
public SearchResultList(List delegate, long totalCount, YPage paging) {
super();
Preconditions.checkNotNull(delegate, "delegate must not be null");
this.delegate = delegate;
this.totalCount = totalCount;
this.paging = paging;
}

@Override
protected List delegate() {
return delegate;
}

public YPage getPaging() {
return paging;
}

public long getTotalCount() {
return totalCount;
}

/**
* get the index of this result's page.
*
* @return index of the page defined by this result
*/
public int getPageIndex() {
return (int) paging.getFirstResultIndex() / paging.getPageSize();
}

/**
* @return page count
*/
public int getPageCount() {
//is i the lastPageIndex ??!!
int i = (int) totalCount / paging.getPageSize();
if (totalCount % paging.getPageSize() != 0) {
i = i + 1;
}
return i;
}

abdelgadir said...

public boolean hasPreviousPage(){
return getPageIndex() > 0;
}

public boolean hasNextPage(){
return ((getPageIndex()+1) * getPaging().getPageSize()) < getTotalCount();
}

@Override
public String toString() {
return "SearchResultList{" +
"paging=" + paging +
", totalCount=" + totalCount +
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;

SearchResultList that = (SearchResultList) o;

if (totalCount != that.totalCount) return false;
if (paging != null ? !paging.equals(that.paging) : that.paging != null) return false;

return true;
}

@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (paging != null ? paging.hashCode() : 0);
result = 31 * result + (int) (totalCount ^ (totalCount >>> 32));
return result;
}
}

abdelgadir said...

correction, google code project which inspired Ypage,YLimit, YOrder is
http://www.altuure.com/projects/yagdao

The YFetch concept is my addition.

abdelgadir said...

and this is the fetch strategy interface


public interface IPersistenceJPAFetchStrategy {
public void applyFetch(YFetch fetch, Query query);
}

abdelgadir said...

one more comment, please note SearchResultList is compatible with List. So any client that expects a List as a result will not be affected.

Thanks

abdelgadir said...

forgot to include the Yfetch class. here it is:


package hospital.model.dao;

import com.google.common.collect.*;

import java.io.Serializable;
import java.util.Collection;
import java.util.Map;


/**
* This class allows you to dynamically add individual fields to the set of fields that will be eagerly
* loaded from the database.
*
* The methods that take only string arguments use the fully-qualified field name, such as org.mag.Magazine.publisher.
* Similarly, getFields returns the set of fully-qualified field names. In all methods, the named field must be
* defined in the class specified in the invocation, not a superclass. So, if the field publisher is defined in base
* class Publication rather than sub-class Magazine, you must invoke addField (Publication.class, "publisher") and not
* addField (Magazine.class, "publisher"). This helps reduce more extensive look-ups for predominant use cases.
*
* Also, in order to avoid the cost of reflection, the system will not perform any validation of the
* field name/class name pairs that you put into the fetch configuration. If you specify non-existent class/field pairs,
* nothing adverse will happen, but you will receive no notification of the fact that the specified configuration is
* not being used.
*/
public class YFetch implements Serializable {
/**
* The maximum depth of relations to traverse when eager fetching. Use -1 for no limit which is the default
* for openJpa.
*/
private int maxFetchDepth = -1;
private Multimap fetchProps = null;

private YFetch() {
this(null, null);
}

/**
* @param clazz the class which directly defines the given fieldName
* @param fieldName the fieldName to eagerly fetch
*/
private YFetch(Class clazz, String fieldName) {
this.fetchProps = HashMultimap.create();
if (clazz != null && fieldName != null) {
this.fetchProps.put(clazz, fieldName);
}
}

public static YFetch with(Class clazz, String fieldName) {
return new YFetch(clazz, fieldName);
}

public static YFetch with() {
return new YFetch();
}

public YFetch and(Class clazz, String fieldName) {
this.fetchProps.put(clazz, fieldName);
return this;
}

public YFetch and(Class clazz, String... fieldNames) {
this.fetchProps.putAll(clazz, Lists.newArrayList(fieldNames));
return this;
}

public YFetch and(Class clazz, Collection fieldNames) {
this.fetchProps.putAll(clazz, fieldNames);
return this;
}

public YFetch setMaxFetchDepth(int maxFetchDepth) {
this.maxFetchDepth = maxFetchDepth;
return this;
}

public int getMaxFetchDepth() {
return maxFetchDepth;
}

public Map> getFetchProperties() {
return fetchProps.asMap();
}

public boolean has(Class clazz, String fieldName) {
return fetchProps.containsEntry(clazz, fieldName);
}

}

abdelgadir said...

yet another suggestion.

Make your query methods accept Map representing the query parameter list.

e.g., I have

public SearchResultList findByNamedQuery(final Class type, final String namedQueryName,
final Map parameters,
final @Nullable YLimit limit, final @Nullable YFetch fetch) {
//namedQueries will only take YLimit (not YPage) because the ordering is already part of the named query
Set> rawParameters = parameters.entrySet();
TypedQuery query = getEntityManager().createNamedQuery(namedQueryName, type);
//process the named parameters
for (Map.Entry entry : rawParameters) {
query.setParameter(entry.getKey(), entry.getValue());
}
return executeQuery(query, limit, fetch);
}


the client can then construct the parameters much more conveniently using the following fluent API



public class QueryParameter {
private Map parameters = null;

private QueryParameter(String name, Object value) {
this.parameters = new HashMap();
this.parameters.put(name, value);
}

public static QueryParameter with(String name, Object value) {
return new QueryParameter(name, value);
}

public QueryParameter and(String name, Object value) {
this.parameters.put(name, value);
return this;
}

public Map parameters() {
return this.parameters;
}
}

abdelgadir said...

I just noticed all generic bracket have been removed from my postings e.g., MAP<String,String&gt becomes MAP :(

Thomas said...

Hey abdelgadir,

Appreciate your efforts! As you've noticed, the blog comments are not very code friendly. Please place feature requests at the project website (https://github.com/ctpconsulting/query) where they are much better kept, structured and tracked than here.

Igor Petruk said...

Is it usable with Guice? If it isn't are you planning to provide some Guice integration?