Cannot use method with argument in mapper file - datamapper

Java 1.7, WildFly 8, MyBatis 3.2.6, PostgreSQL 9.4.
I have prototype that is simple system to lend books. I use MyBatis as ORM. All was fine and dandy until I tried filters.
There is table of books. Getting list of books is defined in mapper like this:
<select id="selectBooks" resultType="org.bookman.json.library.JsonBook">
select id, created, inclusion_date as inclusionDate, title, author
from bok_books
<where>
<if test="filter('title') != null">
title ilike #{filter('title')}||'%'
</if>
</where>
order by ${orderCols}
</select>
There are two methods used in EL - filter(String) and getOrderCols(). Select is called in this way:
TableReqHnd bookHnd = new TableReqHnd();
... // among other things sets filter data
List<JsonBook> books = sqlSession.selectList("selectBooks", bookHnd, rowBounds);
This is how TableReqHnd class looks:
public class TableReqHnd
{
private Map<String, String> filters = new HashMap<>();
...
public String getOrderCols()
{
...
}
...
public String filter(String fieldId)
{
if (!filters.containsKey(fieldId)) return null;
return filters.get(fieldId);
}
}
Why I did it like that? It allows me to reuse TableReqHnd object for any table, just with defined new values for ordering, filters etc. I don't have to create separate class for each table with its own setters and getters for each filter.
Now problem itself. Method getOrderCols() works fine, but filter(String) does not. Here is what exception is thrown: http://pastebin.com/raw.php?i=9jAjBhtd
It throws essentially java.lang.NoSuchMethodException: filter(java.lang.String). I find it strange. This method certainly exists in TableReqHnd: public String filter(String fieldId). ${orderCols} works fine (it is just simple getter public String getOrderCols()).
According to OGNL specs, call
filter('title')
should be correct. Exception in fact confirms it should be possible, it just cannot find method for some reason. Anyone knows if it can be fixed somehow?

Have you try
<select id="selectBooks" paramType="org.bookman.json.library.JsonBook">
select id, created, inclusion_date as inclusionDate, title, author
from bok_books
<where>
<if test="filter('title') != null">
title ilike #{filter('title')}||'%'
</if>
</where>
order by ${orderCols}
</select>

Related

How to do Pagination with mybatis?

I am currently working on a ecommerce application where I have to show a list of available products using search functionality.
As with every search, I have to implement Pagination here.
I am using mybatis as my ORM tool and mysql as an underlying database.
Googling around I found following ways to accomplish this task :
Client Side paging
: Here I will have to fetch all the results from the database matching the search criteria in one stroke and handle the pagination at my code level (Possibly frond end code ).
Server Side Paging :
With mysql I can use the Limit and the offset of the resultset to construct a query like :
SELECT * FROM sampletable WHERE condition1>1 AND condition2>2 LIMIT 0,20
Here, I have to pass the offset and limit count everytime the user selects a new page while navigating in search results.
Can anyone tell,
which will be better way to implement paging ?
Do mybatis supports a better way to implement paging than just relying on above SQL queries ( like the hibernate criteria APIs).
Any inputs is highly appreaciated.
Thanks .
I myself use your second opion with LIMIT in sql query.
But there is range of methods that support pagination using RowBounds class.
This is well described in mybatis documentation here
Pay attention to correct result set type to use.
If you're using Mappers (much easier than using raw SqlSessions), the easiest way to apply a limit is by adding a RowBounds parameter to the mapping function's argument list, e.g:
// without limit
List<Foo> selectFooByExample(FooExample ex);
// with limit
List<Foo> selectFooByExample(FooExample ex, RowBounds rb);
This is mentioned almost as an afterthought in the link Volodymyr posted, under the Using Mappers heading, and could use some more emphasis:
You can also pass a RowBounds instance to the method to limit query results.
Note that support for RowBounds may vary by database. The Mybatis documentation implies that Mybatis will take care of using the appropriate query. However, for Oracle at least, this gets handled by very inefficient repeat calls to the database.
pagination has two types, physical and logical
logical means to retrieve all the data first then sort them in memory
physical means database level subset select
the default mybatis pagination is logical... thus when you select a massive database e.g 100GB of blobs, the rowbound method will still be very slow
the solution is to use the physical pagination
you can do your own way through the mybatis interceptor
or using plugins pre made by someone else
If you are using Spring MyBatis, you can achieve pagination manually using 2 MyBatis queries and the useful Spring Page and Pageable interfaces.
You create a higher level DAO interface e.g. UploadDao
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface UploadDao {
Page<Upload> search(UploadSearch uploadSearch, Pageable pageable);
}
... where Upload maps to an upload table and UploadSearch is a parameter POJO e.g.
#Data // lombok
public class UploadSearch {
private Long userId;
private Long projectId;
...
}
An implementation of UploadDao (which injects a MyBatis UploadMapper mapper) is as follows:
public class DefaultUploadDao implements UploadDao {
#Autowired
private UploadMapper uploadMapper;
public Page<Upload> searchUploads(UploadSearch uploadSearch, Pageable pageable) {
List<Upload> content = uploadMapper.searchUploads(uploadSearch, pageable);
Long total = uploadMapper.countUploads(uploadSearch);
return new PageImpl<>(content, pageable, total);
}
}
The DAO implementation calls 2 methods of UploadMapper. These are:
UploadMapper.searchUploads - returns a page of results based on search param (UploadSearch) and Pageable param (contains offset / limit etc).
UploadMapper.countUploads - returns total count, again based on search param UploadSearch. NOTE - Pageable param is not required here as we're simply determining the total rows the search parameter filters to and don't care about page number / offset etc.
The injected UploadMapper interface looks like ...
#Mapper
public interface UploadMapper {
List<Upload> searchUploads(
#Param("search") UploadSearch search,
#Param("pageable") Pageable pageable);
long countUploads(
#Param("search") UploadSearch search);
}
... and the mapper XML file containing the dynamic SQL e.g. upload_mapper.xml contains ...
<mapper namespace="com.yourproduct.UploadMapper">
<select id="searchUploads" resultType="com.yourproduct.Upload">
select u.*
from upload u
<include refid="queryAndCountWhereStatement"/>
<if test="pageable.sort.sorted">
<trim prefix="order by">
<foreach item="order" index="i" collection="pageable.sort" separator=", ">
<if test="order.property == 'id'">id ${order.direction}</if>
<if test="order.property == 'projectId'">project_id ${order.direction}</if>
</foreach>
</trim>
</if>
<if test="pageable.paged">
limit #{pageable.offset}, #{pageable.pageSize}
</if>
<!-- NOTE: PostgreSQL has a slightly different syntax to MySQL i.e.
limit #{pageable.pageSize} offset #{pageable.offset}
-->
</select>
<select id="countUploads" resultType="long">
select count(1)
from upload u
<include refid="queryAndCountWhereStatement"/>
</select>
<sql id="queryAndCountWhereStatement">
<where>
<if test="search != null">
<if test="search.userId != null"> and u.user_id = #{search.userId}</if>
<if test="search.productId != null"> and u.product_id = #{search.productId}</if>
...
</if>
</where>
</sql>
</mapper>
NOTE - <sql> blocks (along with <include refid=" ... " >) are very useful here to ensure your count and select queries are aligned. Also, when sorting we are using conditions e.g. <if test="order.property == 'projectId'">project_id ${order.direction}</if> to map to a column (and stop SQL injection). The ${order.direction} is safe as the Spring Direction class is an enum.
The UploadDao could then be injected and used from e.g. a Spring controller:
#RestController("/upload")
public UploadController {
#Autowired
private UploadDao uploadDao; // Likely you'll have a service instead (which injects DAO) - here for brevity
#GetMapping
public Page<Upload>search (#RequestBody UploadSearch search, Pageable pageable) {
return uploadDao.search(search, pageable);
}
}
If you are using the MyBatis Generator, you may want to try the Row Bounds plugin from the official site: org.mybatis.generator.plugins.RowBoundsPlugin. This plugin will add a new version of the
selectByExample method that accepts a RowBounds parameter.

Entity Framework 5 - T4 generated context class causing 'duplicate parameter name'

I'm using EF5.0 in an ASP.NET MVC app. My Entity Model is named 'DataModel'. Included in the model is a table-valued function that exists in my MSSQL database, named MatchingEntries. It returns a table of integer ids.
I've looked at the DataModel.Context.cs file, that gets generated via the .tt (T4) template file. It has the following code in it:
[EdmFunction("DataEntities", "MatchingEntries")]
public virtual IQueryable<Nullable<int>> MatchingEntries(string term)
{
var termParameter = term != null ?
new ObjectParameter("Term", term) :
new ObjectParameter("Term", typeof(string));
return ((IObjectContextAdapter)this).ObjectContext.CreateQuery<Nullable<int>>("[DataEntities].[MatchingEntries](#Term)", termParameter);
}
The error I am getting results from using this method twice within the one query, such as:
IQueryable<int> one = db.MatchingEntries("\"one*\"");
IQueryable<int> two = db.MatchingEntries("\"two*\"");
List<int> both = one.Intersect(two).ToList();
The error is:
A parameter named 'Term' already exists in the parameter collection. Parameter names must be unique in the parameter collection.
Parameter name: parameter
Is this a known limitation of the classes generated from an EDMX for table-valued functions? With LINQ2SQL I am able to execute this a a single query to the database (that does a JOIN between the 2 outputs from MatchingEntries) and it replaces the parameter name #Term with #p0 and #p1 for the two different instances of the call. I'd like to make Entity Framework do the same.
So, my question is, how can I get EF to work in the same manner and avoid the 'Duplicate parameter' error?
My fallback is to evaluate each call to db.MatchingEntries separately, by putting ToList() after them. My other idea has been to replace the ObjectParameter name in the T4 generated Context.cs class with something randomly generated each time. These feel like hacks that I should be able to avoid.
This answer is Linq to Entities specific. This doesn't have to be done in Linq to SQL (Linqpad).
Thanks to this question I got a pointer to a viable solution:
extend the autogenerated DBContext class (partial class)
add a method with two parameters in the partial class
at calling, pass an index as second parameter
Detailed Answer:
DataEntitys.my.cs:
[EdmFunction("DataEntities", "MatchingEntries")]
public virtual IQueryable<Nullable<int>> MatchingEntries(string term, int index)
{
string param_name = String.Format("k_{0}", index);
var termParameter = term != null ?
new ObjectParameter(param_name, term) :
new ObjectParameter(param_name, typeof(string));
return ((IObjectContextAdapter)this).
ObjectContext.CreateQuery<Nullable<int>>(
String.Format("[DataEntities].[MatchingEntries](#{0})", param_name),
termParameter);
}
Call the function:
foreach (string teil in such)
{
index++;
if (teil.Trim() != "")
res = res.Join(db.MatchingEntries("\"" + teil + "*\"", index), l => l.ID, s => s.KEY, (l, s) => l);
}

sqlexception index out of bounds with correct sql-statement

i've got an sql statement that works pretty well. but on implementing in my webapp working with play 2.1 i get this error:
javax.persistence.PersistenceException: Query threw SQLException:Column Index out of range, 0 < 1.
i found this question here: Error executing MySQL query via ebean using RawSql
but then i got other exceptions.
i'm trying to get tagged threads that contains a list of tags (same as stack overflow does).
here the sql statement
SELECT t.topic
FROM topic t
WHERE 3 = (SELECT COUNT( DISTINCT ta.id )
FROM topic_tag tt
INNER JOIN tag ta ON ta.id = tt.tag_id
WHERE ta.name IN ('children', 'spain','new')
AND tt.topic_id = t.id )
in play i do this:
RawSql rawSql = RawSqlBuilder.unparsed(sqlString).create();
result = find.setRawSql(rawSql).findList();
then, i got the out of bounds exception. after that i try to set column mappings:
RawSql rawSql = RawSqlBuilder.unparsed(sqlString)
.columnMapping("t.topic","topic")
.columnMapping("t.id","id")
.columnMapping("ta.name","tagList.name")
.columnMapping("ta.id","tagList.id")
.create();
now i get a null pointer exception. probably because ebean can't create a query from that.
here some code from my models:
#Entity
public class Topic extends Model{
#Id
public Long id;
#Required
public String topic;
#ManyToMany
public List<Tag> tagList;
}
#Entity
public class Tag extends Model {
#Id
public long id;
#Required
public String name;
}
after a lot of trying and frustrating i hope that somebody got a hint or a solution for this.
I just wasted few hours with similar problem, I actually managed to solve it by only mapping id field for certain kind of model and selecting lesser amount of fields, other values were automatically loaded after that - So basically, error occurred if I tried to select values like:
.. select e.id, e.name, e.description from exampleTable e .. and use mappings like:
RawSql rawSql = RawSqlBuilder.parse(sql)
// map the sql result columns to bean properties
.columnMapping("e.id", "exampleModel.id")
.columnMapping("e.name", "exampleModel.name")
.columnMapping("e.description", "exampleModel.description")
.create();
When I changed to select only e.id and map:
RawSql rawSql = RawSqlBuilder.parse(sql)
// map the sql result columns to bean properties
.columnMapping("e.id", "exampleModel.id")
.create();
It loaded also e.name and e.description to model values and errors disappeared.
(Of course my own query had several joins and were bit more complicated than this, but basics are the same.)
So to summarize: when this problem occurs, check that you are not loading anything twice (columnMapping), use System.out.println(""); or similar to check which values are already loaded for your model. Remember to also check annotations such as "#JoinColumn" which might load more data under same model - just based on given e.id value. If you dont select and set e.id as columnMapping value, then you might need to list all needed fields separately as .. e.name, e.description ..
Hopefully these findings helps someone out.

Select column from non-generic DbSet?

I want to implement a function that accepts a DbSet (non-generic), a string, and object, and returns DbSet. something like the following pseudu:
public static DbSet Any(DbSet set, string propertyName, objectParameter)
{
var tableName = set.TableName;
var columnName = set.GetColumnNameForProperty(propertyName);
var query = string.Format("SELECT TOP(1) {0} FROM {1} WHERE {0} = {2}",
columnName,
tableName,
objectParameter);
}
I think that SQL query is enough since I'll be able to execute it directly on the Database (context.Database.ExecuteSql).
What I want to do is get the table name from the given DbSet, then the column name in the database.
It is not possible from non generic DbSet but this problem can be easily solved by using:
public static IEnumerable<T> Any(DbSet<T> set, string property, objectParameter)
where T : class
{ ... }
Returning DbSet doesn't make sense because once you query data it is not DbSet anymore.
The bigger problem is getting table name from generic DbSet / ObjectSet because this information is not available from those classes. It is almost impossible to get it at all because it requires accessing non public members of items from MetadataWorkspace.

How to alter this LINQ to SQL so it doesn't bomb at runtime?

public IQueryable<Story> FindAllStories(){
var stories = (from s in db.Stories
orderby s.DateEntered descending
select new Story
{
Title = s.Title,
UserName = s.aspnet_User.UserName
}
);
return stories;
}
I need to pass this as IQueryable so the pagination helper I found online can only pull the items I need. The problem is that at runtime when the helper tries to do source.Count(); the compiler isn't a happy camper because it's an 'explicit contruction of an entity type query'.
How would I alter this LINQ to SQL method so this does not happen?
Also, to help me grasp this, why does the previous code not work and this one does?
public IQueryable<Story> FindAllStories(){
var stories = (from s in db.Stories
orderby s.DateEntered descending
select s);
return stories;
}
Update
I'm beginning to think the way to accomplish this (verified it works) is to create a POCO called UserStory. The new class has 2 properties: one of type Story and the other string UserName. I can then return an IQueryable of UserStory without a problem.
That's great; however, I still don't get why that method would work and my other doesn't. The other is adding a property of string UserName to my partial class Story and passing that object between layers. What's the difference?
The following link is to a blog post tat describes an issue similar to yours. It seems like the solution was to return a type that inherets from Story instead of a Story type:
http://devlicio.us/blogs/derik_whittaker/archive/2008/04/25/linq2sql-explicit-construction-of-entity-exception.aspx
Hope this helps.