2009-12-17 5 views
11

Esistono diverse librerie di builder di query in stile ActiveRecord. Alcuni sono stand alone e alcuni vengono built into frameworks. Tuttavia, hanno davvero problemi con le clausole WHERE e HAVING quando si tratta di SQL complesso. Mettendo da parte altri database, sto cercando di trovare un metodo WHERE() compatibile con MySQL e PostgreSQL che possa risolvere questi problemi attuali.Gestione di clausole WHERE complesse con un generatore di query PHP

Quello che segue è un lungo elenco di idee ed esempi che mostrano il meglio che ho potuto inventare finora. Tuttavia, non riesco a risolvere tutti i casi d'uso e ritengo che la mia soluzione parziale sia sciatta. Chiunque sia in grado di rispondere con qualcosa che risolva tutti questi problemi non solo risponderà a questa domanda, ma sarà responsabile della risoluzione di un problema che ha perseguito le implementazioni PHP per diversi anni.

Operatori comuni

= Equal 
    <> Not Equal 
    > Greater Than 
    < Less Than 
    >= Greater Than Or Equal 
    <= Less Than Or Equal 
    BETWEEN between values on right 
    NOT logical NOT 
    AND logical AND 
    OR logical OR 

esempio in cui le clausole

SELECT ... FROM table... 
    WHERE column = 5 
    WHERE column > 5 
    WHERE column IS NULL 
    WHERE column IN (1, 2, 3) 
    WHERE column NOT IN (1, 2, 3) 
    WHERE column IN (SELECT column FROM t2) 
    WHERE column IN (SELECT c3 FROM t2 WHERE c2 = table.column + 10) 
    WHERE column BETWEEN 32 AND 34 
    WHERE column BETWEEN (SELECT c3 FROM t2 WHERE c2 = table.column + 10) AND 100 
    WHERE EXISTS (SELECT column FROM t2 WHERE c2 > table.column) 

Ci sono molti formati comuni ActiveRecord che la clausola in cui() utilizza nelle diverse attuali biblioteche.

$this->db->where(array('session_id' => '?', 'username' => '?')); 
$this->db->fetch(array($id, $username)); 

// vs with is_int($key) 
$this->db->where(array('session_id', 'username')); 
$this->db->fetch(array($id, $username)); 

// vs with is_string($where) 
$this->db->where('session_id', '?'); 
$this->db->where('username'); 
$this->db->fetch(array($id, $username)); 

// vs with is_array($value) 
$this->db->where('session_id', '?'); 
$this->db->where('username', array('Sam', 'Bob')); 
$this->db->fetch(array($id)); 

Ecco il formato finale che ho finora. Dovrebbe gestire il raggruppamento (...) AND (...) e i parametri associati alle istruzioni preparate ("?" & ": name").

function where($column, $op = '=', $value = '?', $group = FALSE){} 


// Single line 

$this->db->where('column > 5'); 
$this->db->where('column IS NULL'); 

// Column + condition 

$this->db->where('column', '='); 
// WHERE column = ?  (prepared statement) 
$this->db->where('column', '<>'); 
// WHERE column <> ? (prepared statement) 

// Column + condition + values 

$this->db->where('column', '=', 5); 
// // WHERE column = 5 
$this->db->where('column', 'IN', '(SELECT column FROM t2)'); 
// WHERE column IN (SELECT column FROM t2) 
$this->db->where('column', 'IN', array(1,2,3)); 
// WHERE column IN (1, 2, 3) 
$this->db->where('column', 'NOT IN', array(1,2,3)); 
// WHERE column NOT IN (1, 2, 3) 

// column + condition + values + group 
$this->db->where(
    array(
     array('column', '<', 20), 
     array('column', '>', 10) 
    ), 
    NULL, 
    NULL, 
    $group = TRUE 
); 
// WHERE (column < 20 AND column > 10) 

: UPDATE:

Nel corso della mia domanda mi è venuto a rendersi conto che WHERE e condizioni avendo solo diventano più complessi il più profondo si va. Cercando di astrarre anche l'80% delle funzionalità risulterebbe in una libreria enorme solo per DOVE e AVERE. Come sottolinea Bill, questo non è ragionevole per un linguaggio di scripting come PHP.

La soluzione è solo per creare manualmente la parte WHERE della query. Finché si utilizza " attorno alle colonne, è possibile utilizzare la stessa query WHERE in Postgre, SQLite e MySQL poiché utilizzano quasi la stessa sintassi SQL. (Per MySQL devi str_replace() con un segno di spunta`).

Arriva un punto in cui l'astrazione fa più male che aiuta, DOVE le condizioni sono uno di questi luoghi.

risposta

8

ho lavorato un po 'sulla libreria Zend_Db, che include un PHP class for constructing SQL queries.Ho deciso di punt sul tentativo di gestire ogni sintassi SQL immaginabile in WHERE e HAVING clausole, per diverse ragioni:

  • PHP è un linguaggio di scripting che analizza e compila il codice su ogni richiesta (a meno che non si utilizza una cache bytecode) . Quindi l'ambiente PHP è sensibile alle librerie di codice ingombranti - più di Java o C# o Python o di quello che hai. È quindi una priorità elevata mantenere le biblioteche più snelle che possiamo.

    Tutta la libreria Zend_Db su cui ho lavorato era composta da circa 2000 linee di codice PHP. Al contrario, Java Hibernate è nell'ordine delle linee di codice 118K. Ma non è tanto un problema dal momento che una libreria Java è precompilata e non deve essere caricata su ogni richiesta.

  • Le espressioni SQL seguono una grammatica generativa che è più compatta e più facile da leggere e mantenere quella della costruzione basata su PHP mostrata. Imparare la grammatica delle espressioni SQL è molto più semplice che imparare un'API in grado di simularlo. Finisci per sostenere una "grammatica semplificata". Oppure inizia in questo modo e ti ritrovi costretto dalla community di utenti in Feature Creep finché la tua API non sarà particolarmente complessa.

  • Per eseguire il debug di un'applicazione che utilizza tale API, è necessario accedere all'espressione SQL finale, quindi è possibile utilizzare lo leakiest abstraction.

  • L'unico vantaggio dell'utilizzo di un'interfaccia basata su PHP per le espressioni SQL è che supporta il completamento del codice in editor e IDE intelligenti. Ma quando così tanti operatori e operandi usano costanti di stringa come '>=', si guasta qualsiasi intelligenza di completamento del codice.


aggiornamento: "A Farewell to ORMs" Ho appena letto un buon blog articolo Lo scrittore, Aldo Cortesi, suggerisce di usare lo SQL Expression Language in SQLAlchemy di Python. Lo zucchero sintattico e l'overloading degli operatori standard in Python (ma non supportato in PHP) rendono questa soluzione molto efficace per generare query.

Si potrebbe anche guardare il DBIx :: Class di Perl, ma finisce per essere piuttosto brutto.

+0

Ho la sensazione che sia giusto. Più cerchiamo di astrarre alcune cose, più difficile diventa il nostro lavoro. Un esempio calzante è l'uso forzato di BBCode, tessile o markdown perché i programmatori non volevano filtrare correttamente l'HTML. – Xeoncross

+1

Ho pensato che esistono formati di markup semplici perché gli utenti tipici non possono bilanciare i loro tag HTML. :) –

+0

In realtà, hai * * * per '[b] bilanciare [/ b]' il tuo ' comunque'. È solo che i primi programmatori non capivano la codifica e/o l'uscita dall'output, quindi non sapevano cosa fare con l'HTML nei commenti - * così l'hanno cancellato e gli utenti hanno imparato qualcos'altro *! – Xeoncross

1

L'API di SQLAlchemy è la migliore con cui ho lavorato finora. È una libreria Python, ma puoi ancora esserne ispirata. Non è solo per le clausole WHERE --- l'intera query SQL (che si tratti di una selezione o DML) è espressa con una struttura dati che è facilmente modificabile.

(mi riferisco al suo SQL-toolkit, non i ORM-parti. :-)

+0

Non essendo un dev Python Sto avendo un po 'di difficoltà a seguire l'API quando si tratta di [WHERE struttura clausola] [1]. Ti dispiacerebbe aggiungere un esempio alla tua domanda? [1]: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/expressions.html#sqlalchemy.sql.expression.ClauseElement – Xeoncross

+0

http://www.sqlalchemy.org/docs/06 /sqlexpression.html lo copre. –

2

Questo fa parte della mia classe ActiveRecord, non gestire le query sub (io non si preoccupano neppure):

public function Having($data, $operator = 'LIKE', $merge = 'AND') 
{ 
    if (array_key_exists('query', $this->sql) === true) 
    { 
     foreach ($data as $key => $value) 
     { 
      $this->sql['having'][] = ((empty($this->sql['having']) === true) ? 'HAVING' : $merge) . ' ' . $this->Tick($key) . ' ' . $operator . ' ' . $this->Quote($value); 
     } 
    } 

    return $this; 
} 

public function Where($data, $operator = 'LIKE', $merge = 'AND') 
{ 
    if (array_key_exists('query', $this->sql) === true) 
    { 
     foreach ($data as $key => $value) 
     { 
      $this->sql['where'][] = ((empty($this->sql['where']) === true) ? 'WHERE' : $merge) . ' ' . $this->Tick($key) . ' ' . $operator . ' ' . $this->Quote($value); 
     } 
    } 

    return $this; 
} 

Un altra cosa che si può considerare sta avendo un customHaving() e customWhere() metodi.

+1

Grazie per aver condiviso. Tuttavia, cose come 'FRA? AND? ', Le funzioni, le operazioni raggruppate' (? OR (? AND?)) ', Inoltre, non funzionano. – Xeoncross

+0

Dai un'occhiata alla mia domanda in qualche modo correlata: http://stackoverflow.com/questions/1332217/backticking-mysql-entities forse potresti implementare una cosa simile per il metodo Quote(). –

2

So che questo è un post estremamente vecchio, ma ho intenzione di rispondere comunque, perché sto sviluppando le mie lezioni per soddisfare esigenze simili a quello che la domanda chiede.

Dopo averlo esaminato, ho scoperto che il problema con Zend-Db e altri motori simili è che cercano di essere tutto per tutti. Per attirare l'attenzione del pubblico più vasto, è necessario offrire le funzionalità più generali, che diventano la loro rovina per quanto posso vedere (e come spiegato da Bill Karwin).

Una delle complicazioni più ovvie che molti motori fanno è confondere la generazione del codice SQL con la sua esecuzione (rendendo più facile scrivere SQL sporco). In molte applicazioni, è una buona idea separare entrambi in modo esplicito, incoraggiando lo sviluppatore a pensare ad attacchi di iniezione, ecc.

Nella creazione di un motore SQL, la prima cosa da provare è limitare l'ambito di ciò che SQL può produrre il motore. Ad esempio, non dovresti consentire di produrre un select * from table; il motore dovrebbe richiedere allo sviluppatore di definire esplicitamente ciascuna colonna select, where e having. Come altro esempio, è spesso utile richiedere che ogni colonna abbia un alias (normalmente non richiesto dal database).

Si noti che limitare l'SQL in questi modi non limita ciò che si può effettivamente ottenere dal database. Sì, rende la codifica up-front più prolissa a volte, ma rende anche più strutturata, e ti permette di scaricare centinaia di righe di codice della libreria che erano sempre lì solo in primo luogo per gestire complicate eccezioni e fornire (ahem) "flessibilità".

Le librerie che ho scritto finora sono circa 600 righe di codice (circa 170 righe di cui è la gestione degli errori). Si occupa di join ISO, sottointerventi (nelle clausole SELECT, FROM e WHERE), qualsiasi clausola di confronto a 2 facciate, IN, EXISTS e BETWEEN (con istruzioni secondarie nella clausola WHERE). Inoltre crea implicitamente associazioni, invece di iniettare direttamente valori nell'SQL.

Limitazioni (diverse da quelle già citate): l'SQL è scritto espressamente per Oracle. Non testato su qualsiasi altra piattaforma di database.

Sono disposto a condividere il codice, presupponendo che vengano restituiti eventuali miglioramenti.

Come esempio di ciò che le librerie lasciatemi produco, spero che quanto segue è abbastanza semplice per essere intuitivo, mentre anche essere abbastanza complessa per mostrare le potenzialità di espansione:

<?php 
$substmt = new OraSqlStatement; 
$substmt->AddVarcharCol ('value','VALUE') 
     ->AddVarcharCol ('identity','UID',false) 
     ->AddVarcharCol ('type','info_type',false) 
     ->AddFrom ('schemaa.user_propertues','up') 
     ->AddWhere ('AND') 
     ->AddComparison ('UID', '=', 'e.identity', 'column') 
     ->AddComparison ('info_type', '=', 'MAIL_ADDRESS'); 

$stmt = new OraSqlStatement; 
$stmt->AddVarcharCol ('company_id', 'Company') 
    ->AddVarcharCol ('emp_no',  'Emp Id') 
    ->AddVarcharCol ('person_id', 'Pers Id') 
    ->AddVarcharCol ('name',  'Pers Name') 
    ->AddDateCol ('employed_date', 'Entry Date') 
    ->AddDateCol ('leave_date', 'Leave Date') 
    ->AddVarcharCol ('identity', 'User Id') 
    ->AddVarcharCol ('active', 'Active') 
    ->AddVarcharCol ($substmt, 'mail_addy') 
    ->AddFrom ('schemab.employee_tab', 'e') 
    ->AddFrom ('schemaa.users_vw','u','INNER JOIN','u.emp_no=e.emp_number') 
    ->AddWhere ('AND') 
    ->AddComparison ('User Id', '=', 'my_user_id') 
    ->AddSubCondition ('OR') 
    ->AddComparisonNull ('Leave Date', false) 
    ->AddComparisonBetween ('Entry Date', '2011/01/01', '2011/01/31'); 

echo $stmt->WriteSql(); 
var_dump($stmt->GetBindArray()); 
?> 

che produce:

SELECT 
    company_id "Company", emp_no "Emp Id", person_id "Pers Id", name "Pers Name", 
    employed_date "Entry Date", leave_date "Leave Date", identity "User Id", active "Active", 
    (SELECT value "VALUE" FROM schemaa.user_propertues up 
    WHERE upper(identity) = upper(e.identity) 
     AND upper(TYPE) = upper (:var0) 
) "mail_addy" 
FROM 
    schemab.employee_tab e 
     INNER JOIN schemaa.users_vw u ON u.emp_no = e.emp_number 
WHERE 
     upper (identity) = upper (:var1) 
    AND (leave_date IS NOT NULL OR 
     employed_date BETWEEN to_date (:var2,'YYYY/MM/DD') AND to_date (:var3,'YYYY/MM/DD') 
    ) 

Insieme con l'array bind:

array 
    0 => string 'MAIL_ADDRESS' (length=12) 
    1 => string 'my_user_id' (length=10) 
    2 => string '2011/01/01' (length=10) 
    3 => string '2011/01/31' (length=10) 
Problemi correlati