Alexander Becker has done a great job with this Perl script, and as kindly made it available to the community. The set up for integration with DataTables is broken into multiple different sections, each of which is well commented, noting where changes are required for using with your own database.
The Perl script requires an extra parameter to be sent to it (rm), for which we can use fnServerData:
$(document).ready(function() { $('#example').dataTable( { "bProcessing": true, "bServerSide": true, "sAjaxSource": "/cgi-bin/test/datatables/run.cgi", "fnServerData": function ( sSource, aoData, fnCallback, oSettings ) { /* Add some extra data to the sender */ aoData.push( { "name": "rm", "value": "table_data" } ); $.getJSON( sSource, aoData, function (json) { /** * Insert an extra argument to the request: rm. * It's the the name of the CGI form parameter that * contains the run mode name. Its value is the * runmode, that produces the json output for * datatables. **/ fnCallback(json) } ); } } ); } );
This project uses a configuration file to store the connection
information for the DBMS. Below is an example configuration file
for CGI::Application::Plugin::ConfigAuto
.
It's intended to look like a Perl script.
#!/usr/bin/perl use strict; use warnings; my %CFG = ( db => { dsn => 'DBI:mysql:test:localhost', username => 'test', password => 'test', attributes => { RaiseError => 1, AutoCommit => 1, }, } ); \%CFG;
This is the server-side processing script. As it is a cgi executable, it has to be located in the cgi-executables directory of the webserver. Also, the shebang has to match the local perl installation. On unix like OS, the file should be chmodded as executable (chmod 755) and be owned by whoever is running the webserver.
This is an example using the MVC framework CGI::Application and SQL::Abstract, a library to generate SQL from Perl data structures.
#!/usr/bin/perl =head1 COPYRIGHT Script: DataTables server-side script for Perl and MySQL Copyright: 2010 - Alexander Becker License: This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.8 or, at your option, any later version of Perl 5 you may have available. =cut package DataTablesServer; use strict; use warnings; use 5.010; use base qw/CGI::Application/; use CGI::Application::Plugin::ConfigAuto (qw/cfg/); use CGI::Application::Plugin::JSON 'to_json'; use CGI::Application::Plugin::DBH (qw/dbh_config dbh/); use SQL::Abstract::Limit; use Data::Dumper qw/Dumper/; our $VERSION = 0.2; =head1 NAME DataTablesServer - Server-side processing implementation for DataTables =head1 SYNOPSIS use strict; use warnings; use FindBin qw/$Bin/; my $app = DataTablesServer->new( PARAMS => { cfg_file => $Bin . '/config/test.config', }, ); $app->run(); =head1 DESCRIPTION This is a demo package implementing a tiny CGI script, that provides json data for the jQuery plugin DataTables. This is what you specify as "sAjaxSource". =head1 METHODS =cut =head2 cgiapp_init() Open database connection, setup config files, etc. =cut sub cgiapp_init { my $self = shift; # Set some defaults for DFV unless they already exist. $self->param('dfv_defaults') || $self->param('dfv_defaults', { missing_optional_valid => 1, filters => 'trim', msgs => { any_errors => 'some_errors', prefix => 'err_', invalid => 'Invalid', missing => 'Missing', format => '<span class="dfv-errors">%s</span>', }, }); # -- set up database my $db = $self->cfg('db') or die('Missing config param: db'); $self->dbh_config($db->{dsn}, $db->{username}, $db->{password}, $db->{attributes}); } # /cgiapp_init =head2 setup() Defined runmodes. We have exactely 1 runmode, that provides the json data for the table. See table_data() for how this is done. =cut sub setup { my $self = shift; $self->start_mode('table_data'); $self->run_modes([qw/ table_data /]); } # /setup =head2 table_data() Return table content as json. Evaluates query for global filtering and ordering information. The database is queried to collect the data. =cut sub table_data { my $self = shift; my $q = $self->query(); # -- Define the name of the table that holds the data. my $table = 'ajax'; # -- Define the column names of the table. These names have to match the # names of the columns in $table. my @fields = qw/engine browser platform version grade/; # -- Paging my $limit = $q->param('iDisplayLength') || 10; my $offset = 0; if( $q->param('iDisplayStart') ) { $offset = $q->param('iDisplayStart'); } # -- Ordering my @order = $self->_generate_order_clause(); # -- Filtering my %where = $self->_generate_where_clause(); # -- get table contents my @aaData = $self->_get_table_content( $table, \@fields, \%where, \@order, $limit, $offset ); # -- get meta information about the resultset my $iFilteredTotal = $self->_get_filtered_total( $table, \@fields, \%where ); my $iTotal = $self->_get_total_record_count( $table ); # -- build final data structure my %sOutput = ( sEcho => int($q->param('sEcho')), iTotalRecords => int($iTotal), iTotalDisplayRecords => int($iFilteredTotal), aaData => \@aaData, ); return $self->to_json(\%sOutput); } # /table_data =head2 _generate_order_clause() Evaluate query for odering information. If present, generate order clause, if not, returns empty order clause. =cut sub _generate_order_clause { my $self = shift; my $q = $self->query(); my @order = (); if( defined $q->param('iSortCol_0') ){ for( my $i = 0; $i < $q->param('iSortingCols'); $i++ ) { # build direction, must be '-asc' or '-desc' (cf. SQL::Abstract) # we only get 'asc' or 'desc', so they have to be prefixed with '-' my $direction = '-' . $q->param('sSortDir_'.$i); # We only get the column index (starting from 0), so we have to # translate the index into a column name. my $column_name = $self->_fnColumnToField( $q->param('iSortCol_'.$i) ); push @order, { $direction => $column_name }; } } return @order; } # /_generate_order_clause =head2 _generate_where_clause() Evaluate global search information, that is, information by which each field has to be restricted. If present, generate matching conditions for each searchable column (searchability indicated by query parameters) and combine as disjunction (OR). NOTE this does not match the built-in DataTables filtering which does it word by word on any field. It's possible to do here, but concerned about efficiency on very large tables, and MySQL's regex functionality is very limited. =cut sub _generate_where_clause { my $self = shift; my $q = $self->query(); my %where = (); if( defined $q->param('sSearch') ) { my $search_string = $q->param('sSearch'); for( my $i = 0; $i < $q->param('iColumns'); $i++ ) { # Iterate over each column and check if it is searchable. # If so, add a constraint to the where clause restricting the given # column. In the query, the column is identified by it's index, we # need to translates the index to the column name. my $searchable_ident = 'bSearchable_'.$i; if( $q->param($searchable_ident) and $q->param($searchable_ident) eq 'true' ) { my $column = $self->_fnColumnToField( $i ); push @{$where{'-or'}}, { $column => {-like => '%'.$search_string.'%' } }; } } } return %where; } # /_generate_where_clause =head2 _fnColumnToField( $i ) Maps a number to a column name. Column names are a priori knowlegde (= hardcoded). $i might be 0. =cut sub _fnColumnToField { my $self = shift; my $i = shift // die("Missing column index i."); # Note: we could have used an array, but for dispatching purposes, this is # more readable. my %dispatcher = ( # column => 'rowname', 0 => 'engine', 1 => 'browser', 2 => 'platform', 3 => 'version', 4 => 'grade', ); die("No such row index defined.") unless exists $dispatcher{$i}; return $dispatcher{$i}; } # /_fnColumnToField =head2 _get_table_content( $table, \@fields, \%where, \@order, $limit, $offset ) This method returns the data, that will be displayed in the table. Might be an empty array. If there exist results, return value will be a two-dimensonal array. Basically, this method builds the SQL and fetches the results. =cut sub _get_table_content { my $self = shift; my $table = shift or die("Missing table."); my $fields_aref = shift or die("Missing fields."); my $where_href = shift or die("Missing where clause (href)."); my $order_href = shift or die("Missing order clause (href)."); my $limit = shift // die("Missing limit"); my $offset = shift // die("Missing offset"); my $q = $self->query(); my $dbh = $self->dbh(); my $sql = SQL::Abstract::Limit->new( limit_dialect => $dbh ); my ( $stmt, @bind ) = $sql->select( $table, $fields_aref, $where_href, $order_href, $limit, $offset ); my $sth = $dbh->prepare($stmt) or die("Error preparing sql: " . DBI->errstr() . "\nSQL: $sql\n"); my $rv = $sth->execute(@bind) or die("Error executing sql: " . DBI->errstr() . "\nSQL: $sql\nBind: @bind"); my @aaData = (); while( my @a = $sth->fetchrow_array() ) { push @aaData, \@a; } $sth->finish(); # clean up return @aaData; } # /_get_table_content =head2 _get_total_record_count( $table ) Get the number of records in the table, regardless of restrictions of the where clause or the limit clause. Used to display the total number of records without applied filters. =cut sub _get_total_record_count { my $self = shift; my $table = shift or die("Missing table name."); my $dbh = $self->dbh(); my $sql = qq~SELECT COUNT(engine) AS count FROM $table~; my $sth = $dbh->prepare($sql) or die("Error preparing sql: " . DBI->errstr() . "\nSQL: $sql\n"); my $rv = $sth->execute() or die("Error executing sql: " . DBI->errstr() . "\nSQL: $sql\n"); my $cnt = -1; while( my $href = $sth->fetchrow_hashref() ) { $cnt = $href->{count}; } return $cnt; } # /_get_total_record_count =head2 _get_filtered_total( $table, \@fields, \%where ) Get the total number of filtered records (in resprect of filters by the where clause, without limit). This accounts for the "search" field of data tables. =cut sub _get_filtered_total { my $self = shift; my $table = shift or die("Missing table."); my $fields_aref = shift or die("Missing fields."); my $where_href = shift or die("Missing where clause (href)."); my $dbh = $self->dbh(); my $sql = SQL::Abstract::Limit->new( limit_dialect => $dbh ); my ( $stmt, @bind ) = $sql->select( $table, $fields_aref, $where_href ); my $sth = $dbh->prepare($stmt) or die("Error preparing sql: " . DBI->errstr() . "\nSQL: $sql\n"); my $rv = $sth->execute(@bind) or die("Error executing sql: " . DBI->errstr() . "\nSQL: $sql\nBind: @bind"); return $rv; } # /_get_filtered_total =head1 SEE ALSO DataTables, a great jQuery plugin for table data: L<http://www.datatables.net> The german perl community: L<http://de.perl.org> (they speak english, too) L<CGI::Application>, L<SQL::Abstract>, L<ConfigAuto> =head1 AUTHOR Alexander Becker, E<lt>c a p f a n @ g m x . d eE<gt> =head1 COPYRIGHT AND LICENSE Copyright (C) 2009 by Alexander Becker This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.8 or, at your option, any later version of Perl 5 you may have available. =cut 1; use strict; use warnings; use FindBin qw/$Bin/; my $app = DataTablesServer->new( PARAMS => { cfg_file => $Bin . '/config/test.config', }, ); $app->run();