tebowner's avatar

pagination on complex sql

I have a complex sql that produces a large array.

$documents = DB::select( DB::raw($selectstatement), $binders);

$pageNumber = \Request::get('page', 1);

$perpage = \Request::get('perpage', 10);

$slice = array_slice($documents, $perpage * ($pageNumber - 1), $perpage);

$documents = \Paginator::make($slice, count($documents), $perpage);

in Laravel 5 is this the best way to do this? efficiency? is it better to have LIMIT on my SQL statement?

0 likes
21 replies
jackpopp's avatar

Why don't you just cache the queries result for a set period of time?

tebowner's avatar

@jackpopp not sure what you mean... @bingvanmoorsel $documents = DB::select( DB::raw($selectstatement), $binders)->skip(10)->take(5)->get(); this is OK (i dont think so) ? can i use the out of the box pagination display by supplying the value and rendering?

bingvanmoorsel's avatar

I think you should be fine. skip the amount you already had, and take the amount per page of your pagination.

tebowner's avatar

$documents = DB::select( DB::raw($selectstatement), $binders)->skip(10)->take(5)->get(); ... skip fails

bingvanmoorsel's avatar

Cant you use a model? If you use a model Laravel does the pagination part for you ?

bingvanmoorsel's avatar

I dont think you can use skip and take if you use DB::raw, in that case you need to add the limit and offset option to the query yourself.

Dont use DB::raw if you dont need to

jackpopp's avatar

My idea would be when the query is run you cache the result and then you could just look in the cache for that query instead of hitting the database so it would speed things up. You could then just run the paginator on the result, but many that suggestion isn't applicable to your use case.

tebowner's avatar

if you saw this complex sql ... very dynamic and not sure how i could....

bingvanmoorsel's avatar

Could you share your SQL ?

Otherwise, at the end of your SQL add a LIMIT and OFFSET.

tebowner's avatar

select documents.*,metatags_1.value_string as field_1,metatags_2.value_string as field_2,metatags_3.value_integer as field_3,metatags_4.value_string as field_4,metatags_5.value_date as field_5 FROM documents LEFT JOIN metatags as metatags_1 ON ((documents.id = metatags_1.document_id) and (metatags_1.field_id = 1)) LEFT JOIN metatags as metatags_2 ON ((documents.id = metatags_2.document_id) and (metatags_2.field_id = 2)) LEFT JOIN metatags as metatags_3 ON ((documents.id = metatags_3.document_id) and (metatags_3.field_id = 3)) LEFT JOIN metatags as metatags_4 ON ((documents.id = metatags_4.document_id) and (metatags_4.field_id = 4)) LEFT JOIN metatags as metatags_5 ON ((documents.id = metatags_5.document_id) and (metatags_5.field_id = 5)) where (documents.type_id = 2) and (UPPER(metatags_2.value_string) like :search_1_1) and (UPPER(documents.name) like :search_1_2) and ( (metatags_5.value_date >= :search_1_3) and (metatags_5.value_date <= :search_2_3) ) ORDER BY documents.name desc

taijuten's avatar

Going to be honest, a little scared by the table names more than anything!

As mentioned previously, I think without looking at changing the table structure / relationships, you're going to need to insert the offset and limit manually into the query. Of course, if you do this, be very careful of SQL injection.

tebowner's avatar

structure is basically a documents table with a details table metatags. each document is assigned a category/Type. Each Type has Fields. Each Field has a type (int, string, date, etc). Each document has metatags - field id, value for that document... this was the best way i could do the SQL but i am sure it could be done better :)

Type : 1 = Invoice, 2=Mortgage, etc

Fields : 1=Invoice # (typeid = 1), 2=Register Date (type_id=1), 3=Buyer Name (type_id=2)

Document : 1 = 12345.pdf (type_id=1) , 2=M23677.pdf (type_id=2)

Metatags : document_id=1, field_id=1, value_integer=12345 ; document_id=1, field_id=2, value_date=2015-01-01 ; document_id=2, field_id=3, value_integer=23677;

taijuten's avatar

Not sure how open you are to structure reconfigurations, but it would be worth you looking into polymorphism if you are. http://laravel.com/docs/5.0/eloquent#polymorphic-relations

You could have the following models:

  • Document
  • Invoice
  • Mortgage... etc

Where all are a morph of "document". The invoice table would have metadata relating only to invoices, and mortgage meta for mortgages etc.

Doing this, you could fetch all invoices, fetch everything (documents) or whatever you please, and do pagination fairly easily for each of these combinations.

That being said, if you are too far in to the project to make this change, I'd just do the manual pagination insertion.

tebowner's avatar

@taijuten I kind of understand this BUT ... the Invoice and Mortgage example are defined by the user in an individual account... these are NOT object model i can design before but would need to be designed (and altered) dynamically. I would love ot be able to say Invoice:all(); ... To do this as a user created a new Type I would need to build the model?

bingvanmoorsel's avatar

@tebowner I know you wont like the idea of doing that but: Why dont you convert the query into the query builder, without the DB::raw. If you build it in your query builder you can actualy use the ->skip() and ->take(). This makes your query secure aswell.

tebowner's avatar

@bingvanmoorsel the issue is CAN my monster dynamic query fit into a query builder? you can see the query in previous comment

bingvanmoorsel's avatar

@tebowner

i think most of it can be changed to the laravel Querybuilder, everything you cant add normaly ( like the join condistions) you can use the DB::raw.

I also think you can add a OFFSET and LIMIT in your query. get the value's out of your paginator.

let me know ;-)

tebowner's avatar

i dont know ... too confusing to add the joins which grabs the dynamic fields etc etc ... will be looking

kimsyversen's avatar

I had to paginate a many to many relationship and made this code. Hopefully it may be useful for someone. However, it may contain unnoticed bugs, but it seems to work fine). Improvements would be appreciated :)

In controller

public function index(Request $request)
{
    $searchTerm = Input::get('search');

    $searchOperator = Input::get('searchOperator') ? Input::get('searchOperator') : 'LIKE';

    $offset = Input::get('offset') ? Input::get('offset') : 5;
  
    $data =  $this->repository->resultsPaginated($searchOperator, $searchTerm, $offset),

    return view('pages.index', compact('data'));
}

In repository

 public function resultsPaginated($searchOperator, $searchTerm, $offset) {
    $currentPage = LengthAwarePaginator::resolveCurrentPage();

    if($currentPage >= 1)
        $skip = ($currentPage -1) * $offset;
    else
        $skip = 0;

    $query = DB::connection('sqlite_nessus')->table('t1')
        ->join('t2', 't1.id', '=', 't2.t1id')
        ->join('t3', 't3.id', '=', 't2.t3id')
        ->where('t3.someColumn', $searchOperator, $searchTerm);

    $count = $query->count();

    $results = $query
        ->skip($skip)
        ->take($offset)
        ->select('t1.id', 't2.id', 't3.id')
        ->get();
    

    return new LengthAwarePaginator($results, $count, $offset, $currentPage, [
        'path' => LengthAwarePaginator::resolveCurrentPath(),
        'offset' => $offset
    ]);
}

Please or to participate in this conversation.