94516e7f2a
Signed-off-by: Paul Poulain <paul.poulain@biblibre.com>
499 lines
13 KiB
Perl
499 lines
13 KiB
Perl
package C4::SQLHelper;
|
|
|
|
# Copyright 2009 Biblibre SARL
|
|
#
|
|
# This file is part of Koha.
|
|
#
|
|
# Koha is free software; you can redistribute it and/or modify it under the
|
|
# terms of the GNU General Public License as published by the Free Software
|
|
# Foundation; either version 2 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with Koha; if not, write to the Free Software Foundation, Inc.,
|
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
|
|
use strict;
|
|
use warnings;
|
|
use List::MoreUtils qw(first_value any);
|
|
use C4::Context;
|
|
use C4::Dates qw(format_date_in_iso);
|
|
use C4::Debug;
|
|
require Exporter;
|
|
use vars qw($VERSION @ISA @EXPORT_OK %EXPORT_TAGS);
|
|
|
|
eval {
|
|
my $servers = C4::Context->config('memcached_servers');
|
|
if ($servers) {
|
|
require Memoize::Memcached;
|
|
import Memoize::Memcached qw(memoize_memcached);
|
|
|
|
my $memcached = {
|
|
servers => [$servers],
|
|
key_prefix => C4::Context->config('memcached_namespace') || 'koha',
|
|
};
|
|
|
|
memoize_memcached( '_get_columns', memcached => $memcached, expire_time => 600000 ); #cache for 10 minutes
|
|
memoize_memcached( 'GetPrimaryKeys', memcached => $memcached, expire_time => 600000 ); #cache for 10 minutes
|
|
}
|
|
};
|
|
|
|
BEGIN {
|
|
# set the version for version checking
|
|
$VERSION = 0.5;
|
|
require Exporter;
|
|
@ISA = qw(Exporter);
|
|
@EXPORT_OK=qw(
|
|
InsertInTable
|
|
DeleteInTable
|
|
SearchInTable
|
|
UpdateInTable
|
|
GetPrimaryKeys
|
|
clear_columns_cache
|
|
);
|
|
%EXPORT_TAGS = ( all =>[qw( InsertInTable DeleteInTable SearchInTable UpdateInTable GetPrimaryKeys)]
|
|
);
|
|
}
|
|
|
|
my $tablename;
|
|
my $hashref;
|
|
|
|
=head1 NAME
|
|
|
|
C4::SQLHelper - Perl Module containing convenience functions for SQL Handling
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use C4::SQLHelper;
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This module contains routines for adding, modifying and Searching Data in MysqlDB
|
|
|
|
=head1 FUNCTIONS
|
|
|
|
=head2 SearchInTable
|
|
|
|
$hashref = &SearchInTable($tablename,$data, $orderby, $limit,
|
|
$columns_out, $filtercolumns, $searchtype);
|
|
|
|
|
|
$tablename Name of the table (string)
|
|
|
|
$data may contain
|
|
- string
|
|
|
|
- data_hashref : will be considered as an AND of all the data searched
|
|
|
|
- data_array_ref on hashrefs : Will be considered as an OR of Datahasref elements
|
|
|
|
$orderby is an arrayref of hashref with fieldnames as key and 0 or 1 as values (ASCENDING or DESCENDING order)
|
|
|
|
$limit is an array ref on 2 values in order to limit results to MIN..MAX
|
|
|
|
$columns_out is an array ref on field names is used to limit results on those fields (* by default)
|
|
|
|
$filtercolums is an array ref on field names : is used to limit expansion of research for strings
|
|
|
|
$searchtype is string Can be "start_with" or "exact"
|
|
|
|
This query builder is very limited, it should be replaced with DBIx::Class
|
|
or similar very soon
|
|
Meanwhile adding support for special key '' in case of a data_hashref to
|
|
support filters of type
|
|
|
|
( f1 = a OR f2 = a ) AND fx = b AND fy = c
|
|
|
|
Call for the query above is:
|
|
|
|
SearchInTable($tablename, {'' => a, fx => b, fy => c}, $orderby, $limit,
|
|
$columns_out, [f1, f2], 'exact');
|
|
|
|
NOTE: Current implementation may remove parts of the iinput hashrefs. If that is a problem
|
|
a copy needs to be created in _filter_fields() below
|
|
|
|
=cut
|
|
|
|
sub SearchInTable{
|
|
my ($tablename,$filters,$orderby, $limit, $columns_out, $filter_columns,$searchtype) = @_;
|
|
$searchtype||="exact";
|
|
my $dbh = C4::Context->dbh;
|
|
$columns_out||=["*"];
|
|
my $sql = do { local $"=', ';
|
|
qq{ SELECT @$columns_out from $tablename}
|
|
};
|
|
my $row;
|
|
my $sth;
|
|
my ($keys,$values)=_filter_fields($tablename,$filters,$searchtype,$filter_columns);
|
|
if ($keys){
|
|
my @criteria=grep{defined($_) && $_ !~/^\W$/ }@$keys;
|
|
if (@criteria) {
|
|
$sql.= do { local $"=') OR (';
|
|
qq{ WHERE (@criteria) }
|
|
};
|
|
}
|
|
}
|
|
if ($orderby){
|
|
#Order by desc by default
|
|
my @orders;
|
|
foreach my $order ( ref($orderby) ? @$orderby : $orderby ){
|
|
if (ref $order) {
|
|
push @orders,map{ "$_".($order->{$_}? " DESC " : "") } keys %$order;
|
|
} else {
|
|
push @orders,$order;
|
|
}
|
|
}
|
|
$sql.= do { local $"=', ';
|
|
qq{ ORDER BY @orders}
|
|
};
|
|
}
|
|
if ($limit){
|
|
$sql.=qq{ LIMIT }.join(",",@$limit);
|
|
}
|
|
|
|
$debug && $values && warn $sql," ",join(",",@$values);
|
|
$sth = $dbh->prepare_cached($sql);
|
|
eval{$sth->execute(@$values)};
|
|
warn $@ if ($@ && $debug);
|
|
my $results = $sth->fetchall_arrayref( {} );
|
|
return $results;
|
|
}
|
|
|
|
=head2 InsertInTable
|
|
|
|
$data_id_in_table = &InsertInTable($tablename,$data_hashref,$withprimarykeys);
|
|
|
|
Insert Data in table and returns the id of the row inserted
|
|
|
|
=cut
|
|
|
|
sub InsertInTable{
|
|
my ($tablename,$data,$withprimarykeys) = @_;
|
|
my $dbh = C4::Context->dbh;
|
|
my ($keys,$values)=_filter_hash($tablename,$data,($withprimarykeys?"exact":0));
|
|
my $query = qq{ INSERT INTO $tablename SET }.join(", ",@$keys);
|
|
|
|
$debug && warn $query, join(",",@$values);
|
|
my $sth = $dbh->prepare_cached($query);
|
|
eval{$sth->execute(@$values)};
|
|
warn $@ if ($@ && $debug);
|
|
|
|
return $dbh->last_insert_id(undef, undef, $tablename, undef);
|
|
}
|
|
|
|
=head2 UpdateInTable
|
|
|
|
$status = &UpdateInTable($tablename,$data_hashref);
|
|
|
|
Update Data in table and returns the status of the operation
|
|
|
|
=cut
|
|
|
|
sub UpdateInTable{
|
|
my ($tablename,$data) = @_;
|
|
my @field_ids=GetPrimaryKeys($tablename);
|
|
my @ids=@$data{@field_ids};
|
|
my $dbh = C4::Context->dbh;
|
|
my ($keys,$values)=_filter_hash($tablename,$data,0);
|
|
return unless ($keys);
|
|
my $query =
|
|
qq{ UPDATE $tablename
|
|
SET }.join(",",@$keys).qq{
|
|
WHERE }.join (" AND ",map{ "$_=?" }@field_ids);
|
|
$debug && warn $query, join(",",@$values,@ids);
|
|
|
|
my $sth = $dbh->prepare_cached($query);
|
|
my $result;
|
|
eval{$result=$sth->execute(@$values,@ids)};
|
|
warn $@ if ($@ && $debug);
|
|
return $result;
|
|
}
|
|
|
|
=head2 DeleteInTable
|
|
|
|
$status = &DeleteInTable($tablename,$data_hashref);
|
|
|
|
Delete Data in table and returns the status of the operation
|
|
|
|
=cut
|
|
|
|
sub DeleteInTable{
|
|
my ($tablename,$data) = @_;
|
|
my $dbh = C4::Context->dbh;
|
|
my ($keys,$values)=_filter_fields($tablename,$data,1);
|
|
if ($keys){
|
|
my $query = do { local $"=') AND (';
|
|
qq{ DELETE FROM $tablename WHERE (@$keys)};
|
|
};
|
|
$debug && warn $query, join(",",@$values);
|
|
my $sth = $dbh->prepare_cached($query);
|
|
my $result;
|
|
eval{$result=$sth->execute(@$values)};
|
|
warn $@ if ($@ && $debug);
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
=head2 GetPrimaryKeys
|
|
|
|
@primarykeys = &GetPrimaryKeys($tablename)
|
|
|
|
Get the Primary Key field names of the table
|
|
|
|
=cut
|
|
|
|
sub GetPrimaryKeys($) {
|
|
my $tablename=shift;
|
|
my $hash_columns=_get_columns($tablename);
|
|
return grep { $hash_columns->{$_}->{'Key'} =~/PRI/i} keys %$hash_columns;
|
|
}
|
|
|
|
|
|
=head2 clear_columns_cache
|
|
|
|
C4::SQLHelper->clear_columns_cache();
|
|
|
|
cleans the internal cache of sysprefs. Please call this method if
|
|
you update a tables structure. Otherwise, your new changes
|
|
will not be seen by this process.
|
|
|
|
=cut
|
|
|
|
sub clear_columns_cache {
|
|
%$hashref = ();
|
|
}
|
|
|
|
|
|
|
|
=head2 _get_columns
|
|
|
|
_get_columns($tablename)
|
|
|
|
Given a tablename
|
|
Returns a hashref of all the fieldnames of the table
|
|
With
|
|
Key
|
|
Type
|
|
Default
|
|
|
|
=cut
|
|
|
|
sub _get_columns($) {
|
|
my ($tablename) = @_;
|
|
unless ( exists( $hashref->{$tablename} ) ) {
|
|
my $dbh = C4::Context->dbh;
|
|
my $sth = $dbh->prepare_cached(qq{SHOW COLUMNS FROM $tablename });
|
|
$sth->execute;
|
|
my $columns = $sth->fetchall_hashref(qw(Field));
|
|
$hashref->{$tablename} = $columns;
|
|
}
|
|
return $hashref->{$tablename};
|
|
}
|
|
|
|
=head2 _filter_columns
|
|
|
|
=over 4
|
|
|
|
_filter_columns($tablename,$research, $filtercolumns)
|
|
|
|
=back
|
|
|
|
Given
|
|
- a tablename
|
|
- indicator on purpose whether all fields should be returned or only non Primary keys
|
|
- array_ref to columns to limit to
|
|
|
|
Returns an array of all the fieldnames of the table
|
|
If it is not for research purpose, filter primary keys
|
|
|
|
=cut
|
|
|
|
sub _filter_columns ($$;$) {
|
|
my ($tablename,$research, $filtercolumns)=@_;
|
|
if ($filtercolumns){
|
|
return (@$filtercolumns);
|
|
}
|
|
else {
|
|
my $columns=_get_columns($tablename);
|
|
if ($research){
|
|
return keys %$columns;
|
|
}
|
|
else {
|
|
return grep {my $column=$_; any {$_ ne $column }GetPrimaryKeys($tablename) } keys %$columns;
|
|
}
|
|
}
|
|
}
|
|
=head2 _filter_fields
|
|
|
|
_filter_fields
|
|
|
|
Given
|
|
- a tablename
|
|
- a string or a hashref (containing, fieldnames and datatofilter) or an arrayref to one of those elements
|
|
- an indicator of operation whether it is a wide research or a narrow one
|
|
- an array ref to columns to restrict string filter to.
|
|
|
|
Returns a ref of key array to use in SQL functions
|
|
and a ref to value array
|
|
|
|
=cut
|
|
|
|
sub _filter_fields{
|
|
my ($tablename,$filter_input,$searchtype,$filtercolumns)=@_;
|
|
my @keys;
|
|
my @values;
|
|
if (ref($filter_input) eq "HASH"){
|
|
my ($keys, $values);
|
|
if (my $special = delete $filter_input->{''}) { # XXX destroyes '' key
|
|
($keys, $values) = _filter_fields($tablename,$special, $searchtype,$filtercolumns);
|
|
}
|
|
my ($hkeys, $hvalues) = _filter_hash($tablename,$filter_input, $searchtype);
|
|
if ($hkeys){
|
|
push @$keys, @$hkeys;
|
|
push @$values, @$hvalues;
|
|
}
|
|
if ($keys){
|
|
my $stringkey="(".join (") AND (",@$keys).")";
|
|
return [$stringkey],$values;
|
|
}
|
|
else {
|
|
return ();
|
|
}
|
|
} elsif (ref($filter_input) eq "ARRAY"){
|
|
foreach my $element_data (@$filter_input){
|
|
my ($localkeys,$localvalues)=_filter_fields($tablename,$element_data,$searchtype,$filtercolumns);
|
|
if ($localkeys){
|
|
@$localkeys=grep{defined($_) && $_ !~/^\W*$/}@$localkeys;
|
|
my $string=do{
|
|
local $"=") OR (";
|
|
qq{(@$localkeys)}
|
|
};
|
|
push @keys, $string;
|
|
push @values, @$localvalues;
|
|
}
|
|
}
|
|
}
|
|
else{
|
|
$debug && warn "filterstring : $filter_input";
|
|
my ($keys, $values) = _filter_string($tablename,$filter_input, $searchtype,$filtercolumns);
|
|
if ($keys){
|
|
my $stringkey="(".join (") AND (",@$keys).")";
|
|
return [$stringkey],$values;
|
|
}
|
|
else {
|
|
return ();
|
|
}
|
|
}
|
|
|
|
return (\@keys,\@values);
|
|
}
|
|
|
|
sub _filter_hash{
|
|
my ($tablename,$filter_input, $searchtype)=@_;
|
|
my (@values, @keys);
|
|
my $columns= _get_columns($tablename);
|
|
my @columns_filtered= _filter_columns($tablename,$searchtype);
|
|
|
|
#Filter Primary Keys of table
|
|
my $elements=join "|",@columns_filtered;
|
|
foreach my $field (grep {/\b($elements)\b/} keys %$filter_input){
|
|
## supposed to be a hash of simple values, hashes of arrays could be implemented
|
|
$filter_input->{$field}=format_date_in_iso($filter_input->{$field})
|
|
if $columns->{$field}{Type}=~/date/ &&
|
|
$filter_input->{$field} !~C4::Dates->regexp("iso");
|
|
my ($tmpkeys, $localvalues)=_Process_Operands($filter_input->{$field},"$tablename.$field",$searchtype,$columns);
|
|
if (@$tmpkeys){
|
|
push @values, @$localvalues;
|
|
push @keys, @$tmpkeys;
|
|
}
|
|
}
|
|
if (@keys){
|
|
return (\@keys,\@values);
|
|
}
|
|
else {
|
|
return ();
|
|
}
|
|
}
|
|
|
|
sub _filter_string{
|
|
my ($tablename,$filter_input, $searchtype,$filtercolumns)=@_;
|
|
return () unless($filter_input);
|
|
my @operands=split /\s+/,$filter_input;
|
|
|
|
# An act of desperation
|
|
$searchtype = 'contain' if @operands > 1 && $searchtype =~ /start_with/o;
|
|
|
|
my @columns_filtered= _filter_columns($tablename,$searchtype,$filtercolumns);
|
|
my $columns= _get_columns($tablename);
|
|
my (@values,@keys);
|
|
foreach my $operand (@operands){
|
|
my @localkeys;
|
|
foreach my $field (@columns_filtered){
|
|
my ($tmpkeys, $localvalues)=_Process_Operands($operand,"$tablename.$field",$searchtype,$columns);
|
|
if ($tmpkeys){
|
|
push @values,@$localvalues;
|
|
push @localkeys,@$tmpkeys;
|
|
}
|
|
}
|
|
my $sql= join (' OR ', @localkeys);
|
|
push @keys, $sql;
|
|
}
|
|
|
|
if (@keys){
|
|
return (\@keys,\@values);
|
|
}
|
|
else {
|
|
return ();
|
|
}
|
|
}
|
|
sub _Process_Operands{
|
|
my ($operand, $field, $searchtype,$columns)=@_;
|
|
my @values;
|
|
my @tmpkeys;
|
|
my @localkeys;
|
|
|
|
$operand = [$operand] unless ref $operand eq 'ARRAY';
|
|
foreach (@$operand) {
|
|
push @tmpkeys, " $field = ? ";
|
|
push @values, $_;
|
|
}
|
|
#By default, exact search
|
|
if (!$searchtype ||$searchtype eq "exact"){
|
|
return \@tmpkeys,\@values;
|
|
}
|
|
my $col_field=(index($field,".")>0?substr($field, index($field,".")+1):$field);
|
|
if ($field=~/(?<!zip)code|(?<!card)number/ && $searchtype ne "exact"){
|
|
push @tmpkeys,(" $field= '' ","$field IS NULL");
|
|
}
|
|
if ($columns->{$col_field}->{Type}=~/varchar|text/i){
|
|
my @localvaluesextended;
|
|
if ($searchtype eq "contain"){
|
|
foreach (@$operand) {
|
|
push @tmpkeys,(" $field LIKE ? ");
|
|
push @localvaluesextended,("\%$_\%") ;
|
|
}
|
|
}
|
|
if ($searchtype eq "field_start_with"){
|
|
foreach (@$operand) {
|
|
push @tmpkeys,("$field LIKE ?");
|
|
push @localvaluesextended, ("$_\%") ;
|
|
}
|
|
}
|
|
if ($searchtype eq "start_with"){
|
|
foreach (@$operand) {
|
|
push @tmpkeys,("$field LIKE ?","$field LIKE ?");
|
|
push @localvaluesextended, ("$_\%", " $_\%") ;
|
|
}
|
|
}
|
|
push @values,@localvaluesextended;
|
|
}
|
|
push @localkeys,qq{ (}.join(" OR ",@tmpkeys).qq{) };
|
|
return (\@localkeys,\@values);
|
|
}
|
|
1;
|
|
|