I have this problem:
When I try to implement Ayende's complex searching found at:
http://ayende.com/Blog/archive/2006/12/07/ComplexSearchingQueryingWithNHibernate.aspx
with the object graph:
Person:
M:1 Address:
M:1 Street:
M:1 Place:
M:1
Country
I get the following error: NHibernate.QueryException: Cannot use
subqueries on a criteria without a projection.
I am doing this:
public List<Person> Find()
{
DetachedCriteria query = DetachedCriteria.For<Person>();
AddAddressQuery(query);
return personRepository.Find(query);
}
private void AddAddressQuery(DetachedCriteria query)
{
DetachedCriteria addressQuery = null;
if (!String.IsNullOrEmpty(SearchParams.HouseNumer))
{
addresaQuery = DetachedCriteria.For<Address>();
addresaQuery.Add(Restrictions.Eq("HouseNumer",
SearchParams.HouseNumer));
}
this.AddStreetQuery(ref addressQuery);
if (addressQuery != null)
{
query.CreateCriteria("Address1",
"address1").Add(Subqueries.Exists(addressQuery));
}
}
private void AddStreetQuery(ref DetachedCriteria query)
{
DetachedCriteria streetQuery = null;
if (this.SearchParams.StreetId.HasValue)
{
streetQuery = DetachedCriteria.For<Street>();
streetQuery .Add( Restrictions.Eq("Id",
this.SearchParams.StreetId.Value));
}
if (streetQuery != null)
{
query = query ?? Query.CreateCriteria("Address1");
query.CreateCriteria("Street",
"street").Add(Subqueries.Exists(streetQuery ));
}
}
What Am I doing wrong?
Please help
Just like the error message - you need to set a projection for any subqueries.
Your variable addressQuery, a DetachedCriteria, is used as a subquery, but it doesn't have a projection. The relevant portion of the query, when converted to SQL, would look like this:
... EXISTS(SELECT FROM Address WHERE HouseNumber = #HouseNumber)
... which is invalid SQL because no columns (a.k.a projections) have been specified in the select clause.
Use SetProjection to specify the columns.
Related
The JpaRepository allows for executing some SQL queries without having to specify these specifically. For example, it is possible to execute the following method: myRepository.existsById(id).
I would like to execute this method, but instead of checking for the existence of the id, I would like to check for a different property, which I call employeeId. I am aware that I will have to specify this query; this is what I have tried:
#Query("SELECT 1 FROM MyTable t WHERE t.employeeId = ?1")
Integer existsByEmployeeId(Long id);
I am calling this method just before executing a DELETE query:
public Long deleteEntryByEmployeeId(Long id) {
if (myRepository.existsByEmployeeId(id) == 1) {
myRepository.deleteEntryByEmployeeId(id);
return id;
}
return null;
}
This doesn't really work as expected though, as I am returned the error:
Cannot invoke "java.lang.Integer.intValue()" because the return value of "myRepository.existsByEmployeeId(java.lang.Long)" is null
I understand that myRepository.existsByEmployeeId(id) returns null. This seems to be the case if there is no entry in myTable for the specified id. Anyways, I am looking for a smooth solution to this problem. Can somebody help?
There are multiple approaches.
You want to stick with declarative #Query annotation.
#Query("SELECT CASE WHEN count(t) = 1 THEN TRUE ELSE FALSE END FROM MyTable t WHERE t.employeeId = ?1")
Integer existsByEmployeeId(Long id);
It's OK to implement custom functionality.
public interface CustomEmployeeRepository {
boolean existsByEmployeeId(Long id);
}
#Repository
#Transactional(readOnly = true)
class CustomEmployeeRepository implements CustomEmployeeRepository {
#PersistenceContext
private EntityManager em;
#Override
boolean existsByEmployeeId(Long id) {
List list = em.createQuery("SELECT 1 FROM MyTable t WHERE t.employeeId = :id")
.setParameter("id", id)
.getResultList();
return list.size() == 1;
}
}
// inject EmployeeRepository and call existsByEmployeeId as usual
public interface EmployeeRepository extends JpaRepository<Employee, Long>, CustomEmployeeRepository {
}
I have this method on my Dao class:
public List<E> search(String key, Object value) {
EntityManager entityManager = getEntityManager();
entityManager.getTransaction().begin();
List result = entityManager.createQuery("SELECT a FROM "+clazz.getSimpleName()+" a WHERE a."+key+" LIKE '"+value+"%'").getResultList();
entityManager.getTransaction().commit();
entityManager.close();
return result;
}
the sql works fine when the attribute is #Column or a #OneToOne`, but when it's something like that:
#OneToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
#OrderColumn
private List<Titulo> nome;
where the class Titulo has this attributes:
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#Column
private String idioma;
#Column(length=32)
private String conteudo;
causes this error:
message: left and right hand sides of a binary logic operator were incompatible [java.util.List(org.loja.model.categoria.Categoria.nome) : string]; nested exception is org.hibernate.TypeMismatchException: left and right hand sides of a binary logic operator were incompatible [java.util.List(org.loja.model.categoria.Categoria.nome) : string]
How I can change the method to make work for both types of attributes?
I manage to solve this issue with the approach below, using java reflection to detect the type of the field trying to be queried, and using a proper sql command. Don't know how efficient this can be; if anyone have a better solution to this, feel free to add another answer with it.
public List<E> search(String key, Object value) throws NoSuchFieldException {
EntityManager entityManager = getEntityManager();
entityManager.getTransaction().begin();
List result;
Field field = clazz.getDeclaredField(key);
ParameterizedType listType = (ParameterizedType) field.getGenericType();
Class<?> classElement = (Class<?>) listType.getActualTypeArguments()[0];
String nome = classElement.getSimpleName();
Field field2[] = classElement.getDeclaredFields();
String attr = field2[field2.length - 1].getName();
if(field != null) {
if(field2 != null) {
result = entityManager.createQuery("SELECT a FROM "+clazz.getSimpleName()+" a, "+nome+" b WHERE b."+attr+" LIKE '"+value+"%'").getResultList();
} else {
result = entityManager.createQuery("SELECT a FROM "+clazz.getSimpleName()+" a, "+nome+" b WHERE b LIKE '"+value+"%'").getResultList();
}
} else {
result = entityManager.createQuery("SELECT a FROM "+clazz.getSimpleName()+" a WHERE a."+key+" LIKE '"+value+"%'").getResultList();
}
entityManager.getTransaction().commit();
entityManager.close();
return result;
}
UPDATE
I got one issue with the code above: in the first query (of the three in the if/else), it's always returned all the elements of the table, almost if the LIKE was being ignored.
I have data model like this:
class Hand {
public int id;
...
}
class Person {
public int id;
public string name;
public IList<Hand> hands;
...
}
To get data from database, I do this:
ICriteria criteria = databaseSession.CreateCriteria(typeof(Person));
ProjectionList projections = Projections.ProjectionList();
projections
.Add(Projections.Property("id").As("id"))
.Add(Projections.Property("name").As("name"))
.Add(Projections.Property("hands").As("hands"));
projections.Add(Projections.GroupProperty("id"));
projections.Add(Projections.Count("id"), "count");
criteria.SetProjection(projections);
criteria.SetResultTransformer(
NHibernate.Transform.Transformers.AliasToBean(typeof(PersonDTO)));
But NHibernate does not load nested objects in hands property. It just gives null.
Can anyone help me how to get nested objects filled as well (for more than one level depth). Using projections instead of query would be better for me.
Note: It would not be issue in the mapping, because when I loaded data without any projection, it worked well.
a possible solution
var query = databaseSession.CreateCriteria(typeof(Person))
.JoinAlias("hands", "hand")
.SetProjection(Projections.ProjectionList()
.Add(Projections.Property("Id"))
.Add(Projections.Property("Name"))
.Add(Projections.Property("hand.Id"))
.Add(Projections.Property("hand.Foo")))
.List<object[]>()
.GroupBy(arr => (int)arr[0])
.Select(g => new PersonDTO
{
Id = g.Key,
Name = g.First().Name,
Hands = g.Select(arr => new Hand { Id = arr[2], Foo = arr[3] }).ToList(),
});
var results = query.ToList();
I am getting an error when trying to use linqsql query:
LINQ to Entities does not recognize the method 'System.String
get_Item(System.String)' method, and this method cannot be translated
into a store expression.
Code:
[HttpPost]
public ActionResult CreateGiftVoucher(FormCollection collection)
{
IVoucherRepository voucherResp = new VoucherRepository();
IQueryable<Voucher> getVoucher = voucherResp.GetAllVouchers();
//if (getVoucher.Where(x => x.Code == collection["Code"]).Count() > 0) {
if (getVoucher.Any(r => r.Code == collection["Code"]))
{
ModelState.AddModelError("Code", "Code Already Exists");
} return View(); }
Voucher Repository
public IQueryable<Voucher> GetAllVouchers()
{
return entity.Vouchers;
}
Your Linq query is translated to SQL, then executed on the database. But there is no SQL equivalent for collection["Code"], so the query can't be translated to SQL, hence the error. In that case the fix is easy: just put the result of collection["Code"] in a local variable outside the query.
string code = collection["Code"];
if (getVoucher.Any(r => r.Code == code))
{
ModelState.AddModelError("Code", "Code Already Exists");
}
LINQ to entities cannot evaluate the expression collection["Code"]. Try evaluating that expression before the query:
string code = collection["Code"];
if (getVoucher.Any(r => r.Code == code))
{
...
given a query in the form of an ICriteria object, I would like to use NHibernate (by means of a projection?) to find an element's order,
in a manner equivalent to using
SELECT ROW_NUMBER() OVER (...)
to find a specific item's index in the query.
(I need this for a "jump to page" functionality in paging)
any suggestions?
NOTE: I don't want to go to a page given it's number yet - I know how to do that - I want to get the item's INDEX so I can divide it by page size and get the page index.
After looking at the sources for NHibernate, I'm fairly sure that there exists no such functionality.
I wouldn't mind, however, for someone to prove me wrong.
In my specific setting, I did solve this problem by writing a method that takes a couple of lambdas (representing the key column, and an optional column to filter by - all properties of a specific domain entity). This method then builds the sql and calls session.CreateSQLQuery(...).UniqueResult(); I'm not claiming that this is a general purpose solution.
To avoid the use of magic strings, I borrowed a copy of PropertyHelper<T> from this answer.
Here's the code:
public abstract class RepositoryBase<T> where T : DomainEntityBase
{
public long GetIndexOf<TUnique, TWhere>(T entity, Expression<Func<T, TUnique>> uniqueSelector, Expression<Func<T, TWhere>> whereSelector, TWhere whereValue) where TWhere : DomainEntityBase
{
if (entity == null || entity.Id == Guid.Empty)
{
return -1;
}
var entityType = typeof(T).Name;
var keyField = PropertyHelper<T>.GetProperty(uniqueSelector).Name;
var keyValue = uniqueSelector.Compile()(entity);
var innerWhere = string.Empty;
if (whereSelector != null)
{
// Builds a column name that adheres to our naming conventions!
var filterField = PropertyHelper<T>.GetProperty(whereSelector).Name + "Id";
if (whereValue == null)
{
innerWhere = string.Format(" where [{0}] is null", filterField);
}
else
{
innerWhere = string.Format(" where [{0}] = :filterValue", filterField);
}
}
var innerQuery = string.Format("(select [{0}], row_number() over (order by {0}) as RowNum from [{1}]{2}) X", keyField, entityType, innerWhere);
var outerQuery = string.Format("select RowNum from {0} where {1} = :keyValue", innerQuery, keyField);
var query = _session
.CreateSQLQuery(outerQuery)
.SetParameter("keyValue", keyValue);
if (whereValue != null)
{
query = query.SetParameter("filterValue", whereValue.Id);
}
var sqlRowNumber = query.UniqueResult<long>();
// The row_number() function is one-based. Our index should be zero-based.
sqlRowNumber -= 1;
return sqlRowNumber;
}
public long GetIndexOf<TUnique>(T entity, Expression<Func<T, TUnique>> uniqueSelector)
{
return GetIndexOf(entity, uniqueSelector, null, (DomainEntityBase)null);
}
public long GetIndexOf<TUnique, TWhere>(T entity, Expression<Func<T, TUnique>> uniqueSelector, Expression<Func<T, TWhere>> whereSelector) where TWhere : DomainEntityBase
{
return GetIndexOf(entity, uniqueSelector, whereSelector, whereSelector.Compile()(entity));
}
}
public abstract class DomainEntityBase
{
public virtual Guid Id { get; protected set; }
}
And you use it like so:
...
public class Book : DomainEntityBase
{
public virtual string Title { get; set; }
public virtual Category Category { get; set; }
...
}
public class Category : DomainEntityBase { ... }
public class BookRepository : RepositoryBase<Book> { ... }
...
var repository = new BookRepository();
var book = ... a persisted book ...
// Get the index of the book, sorted by title.
var index = repository.GetIndexOf(book, b => b.Title);
// Get the index of the book, sorted by title and filtered by that book's category.
var indexInCategory = repository.GetIndexOf(book, b => b.Title, b => b.Category);
As I said, this works for me. I'll definitely tweak it as I move forward. YMMV.
Now, if the OP has solved this himself, then I would love to see his solution! :-)
ICriteria has this 2 functions:
SetFirstResult()
and
SetMaxResults()
which transform your SQL statement into using ROW_NUMBER (in sql server) or limit in MySql.
So if you want 25 records on the third page you could use:
.SetFirstResult(2*25)
.SetMaxResults(25)
After trying to find an NHibernate based solution for this myself, I ultimately just added a column to the view I happened to be using:
CREATE VIEW vw_paged AS
SELECT ROW_NUMBER() OVER (ORDER BY Id) AS [Row], p.column1, p.column2
FROM paged_table p
This doesn't really help if you need complex sorting options, but it does work for simple cases.
A Criteria query, of course, would look something like this:
public static IList<Paged> GetRange(string search, int rows)
{
var match = DbSession.Current.CreateCriteria<Job>()
.Add(Restrictions.Like("Id", search + '%'))
.AddOrder(Order.Asc("Id"))
.SetMaxResults(1)
.UniqueResult<Paged>();
if (match == null)
return new List<Paged>();
if (rows == 1)
return new List<Paged> {match};
return DbSession.Current.CreateCriteria<Paged>()
.Add(Restrictions.Like("Id", search + '%'))
.Add(Restrictions.Ge("Row", match.Row))
.AddOrder(Order.Asc("Id"))
.SetMaxResults(rows)
.List<Paged>();
}