views:

854

answers:

3

Have just updated from Subsonic 2.2 ActiveRecord to 3.0.0.3. I am trying to use LINQ to do a paged Find query like this (my object/table is called "Repository"):

Repository.Find(item => item.DocumentTitle.Contains(searchTerm))
    .OrderBy(i => i.DocumentTitle).Skip((currentPage - 1) * itemsPerPage)
    .Take(itemsPerPage);

When I view the SQL generated by this query using SQL Server Profiler, there is no paging in the SQL, all the paging is being done in memory in C#. Now, the Subsonic query language does have a nice GetPaged procedure that does work right, but I thought that LINQ was supposed to do this as well. Have I missed something here or is this a limitation of LINQ?

I am aware of the Repository.GetPaged() function, but that doesn't have enough parameters - I need to do a dynamic sort, as well as a Find().

+1  A: 

You can sort GetPaged by adding "desc" to the sort field, but...

Paging should work - I'm looking at the paging SQL in front of me and it's not done in memory. How are you testing this? If you use "ToList()" that will execute the query - have a look at the profiler then.

Rob Conery
The GetPaged function would be ideal, except that I need it to execute a Find() as well. So FindGetPaged() if you will.this is the SQL i see coming through from the query I posted (I also tried ToList<Repository>(), I had been passing the IEnumerable to a ListView datasource):exec sp_executesql N'SELECT [t0].[CategoryName], [t0].[DateCreated], [t0].[DocumentFileType], [t0].[DocumentTitle], [t0].[FileContent], [t0].[RepositoryId]FROM [dbo].[mag_Repository] AS t0WHERE ([t0].[DocumentTitle] LIKE ''%'' + @p0 + ''%'')',N'@p0 nvarchar(4000)',@p0=N''
Steve
To test it I attached the VS debugger, started the profiler only for my DB, and then break at this function:List<Repository> retList = Repository .Find(item => item.DocumentTitle.Contains(searchTerm)) .OrderByDescending(i => i.DocumentTitle) .Skip((currentPage - 1) * itemsPerPage) .Take(itemsPerPage).ToList<Repository>();and watch the sql statement come through the profile when I hit F10.
Steve
I also did a test of GetPaged just to make sure I was seeing the SQL right, and it came through properly with the RowNumber() paging sql statement. So I'm at a loss to understand what I'm doing wrong here.
Steve
+5  A: 

Upon doing further testing, this statement works correctly:

(from i in dataContext.Repositories 
 where i.DocumentTitle.Contains(searchTerm) 
 orderby i.DateCreated ascending select i)
 .Skip((currentPage - 1) * itemsPerPage).Take(itemsPerPage);

When executed, the above linq statement comes back properly paged in sql.

The only conclusion that I can come to is that when you are using method chaining syntax, once you are outside the initial lamda expression

Repository.Find(item => item.DocumentTitle.Contains(searchTerm))

the subsonic SQL interpreter stops creating SQL for any methods chained on the end

.OrderBy(i => i.DocumentTitle).Skip(15).Take(10);

Or, am I just totally doing something wrong here? Anybody have some insight?

Steve
Did even more research. Apparently, this is due to how C# compiles a linq expression. The compiler must know at design-time what the statement is going to be. If it does not, the statement is unable to be reflected by the SubSonic "ExpressionVisitor" (this is not unique to SubSonic) because it no longer a single "Expression".
Steve
There are third party libraries out there that would allow you to build your linq statement in multiple steps, but that adds more external dependencies to your project. SubSonic actually appears to have some of these functions already, but I don't know enough at the moment so suggest a way to use them to accomplish this.
Steve
A: 

A bit late but ...

Repository.Find()

returns IList so the query is executed, hence SQL executing without paging then

.Skip(x).Take(x)

is done in memory. Try

Repository.All().Where(expression).Skip(x).Take(x)

all of which return IQueryable and non of which enumerate the objects and so paging is done in SQL using the ROW_NUMBER() function.

Having said that Subsonic 3 simple repository is generating the following SQL

exec sp_executesql N'SELECT [t0].[Id], [t0].[IsDeleted], [t0].[Name], [t0].[ParentUuid], [t0].[Uuid]
FROM ( SELECT [t1].[Id], [t1].[IsDeleted], [t1].[Name], [t1].[ParentUuid], ROW_NUMBER() OVER() AS rownum, [t1].[Uuid]
FROM [Sites] AS t1
WHERE (([t1].[ParentUuid] = @p0) AND ([t1].[IsDeleted] = 0))) AS t0
WHERE [t0].[rownum] BETWEEN (20 + 1) AND (20 + 10)',N'@p0 uniqueidentifier',@p0='00000000-0000-0000-0000-000000000000'

which throws an exception

Unhandled Exception: System.Data.SqlClient.SqlException: The ranking function "ROW_NUMBER" must have an ORDER BY clause.

so it would seem that there is a bug in Subsonic :-(

Scott Rickman
I wouldn't say that's a SubSonic bug because SubSonic translates your query to plain sql. Since the exceptions tells you ROW_NUMBER must have an ORDER BY clause you have to add an orderby keyword to your linq query as well so subsonic generates an order by, too (btw. mysql wouldn't throw an exception in this case because LIMIT(x,y) does not require an ORDER BY
SchlaWiener
Maybe not a bug, agreed. I've updated a local build of subsonic to test whether an order by clause has been added to any query that contains the ROW_NUMBER function, if there's no order by I insert ORDER BY Id into the OVER part of ROW_NUMBER thereby avoiding the exception. It isn't ideal and it is environment specific (ALL my DOM classes MUST inherit DomBase and WILL have an ID property).
Scott Rickman