Firebird a lastInsertId()
- chloris
- Člen | 23
Ahoj,
používám v jedné aplikaci databázi Firebird, která v PDO nemá podporu pro lastInsertId().Při pokusu o zavolání metody PHPko zakřičí:
Driver does not support this function: driver does not support lastInsertId()
Bohužel v Selection:737 se tahle funkce volá vždy:
$primaryKey = $this->connection->getInsertId($this->getPrimarySequence());
Takže insert vždycky spadne na té chybě.
Příklad takového insertu za předpokladu že tabulka má sloupce ID,MESSAGE,CODE:
$this->db->table('ERROR_LOG')->insert(array(
'MESSAGE' => $message,
'CODE' => $code
));
Nebylo by dobré do driverů do metod isSupported zavést také LAST_INSERT_ID a v případě, že nějaký DB stroj to nepodporuje se na IDčko nedotazovat nebo resp. mít možnost se třeba dotázat jinak? Viz http://www.firebirdfaq.org/faq243/ a klauzule RETURNING
Díky
Ondra
- chloris
- Člen | 23
Navrhuju tedy následující úpravu:
ISupplementalDriver konstanty rozšířit (spodní dvě):
const SUPPORT_SEQUENCE = 'sequence',
SUPPORT_SELECT_UNGROUPED_COLUMNS = 'ungrouped_cols',
SUPPORT_MULTI_INSERT_AS_SELECT = 'insert_as_select',
SUPPORT_MULTI_COLUMN_AS_OR_COND = 'multi_column_as_or',
SUPPORT_SUBSELECT = 'subselect',
SUPPORT_LAST_INSERT_ID = 'last_insert_id',
SUPPORT_INSERT_RETURNING = 'insert_returning';
a metodu insert ve třídě Selection:
/**
* Inserts row in a table.
* @param array|\Traversable|Selection array($column => $value)|\Traversable|Selection for INSERT ... SELECT
* @return IRow|int|bool Returns IRow or number of affected rows for Selection or table without primary key
*/
public function insert($data)
{
if ($data instanceof Selection) {
$data = new Nette\Database\SqlLiteral($data->getSql(), $data->getSqlBuilder()->getParameters());
} elseif ($data instanceof \Traversable) {
$data = iterator_to_array($data);
}
$driver = $this->getConnection()->getSupplementalDriver();
// is lastInsertId() supported ?
if ($driver->isSupported(ISupplementalDriver::SUPPORT_LAST_INSERT_ID)) {
$return = $this->connection->query($this->sqlBuilder->buildInsertQuery(), $data);
$this->loadRefCache();
if ($data instanceof Nette\Database\SqlLiteral || $this->primary === NULL) {
unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
return $return->getRowCount();
}
$primaryKey = $this->connection->getInsertId($this->getPrimarySequence());
if ($primaryKey === FALSE) {
unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
return $return->getRowCount();
}
if (is_array($this->getPrimary())) {
$primaryKey = array();
foreach ((array) $this->getPrimary() as $key) {
if (!isset($data[$key])) {
return $data;
}
$primaryKey[$key] = $data[$key];
}
}
// is insert returning supported ?
} elseif ($driver->isSupported(ISupplementalDriver::SUPPORT_INSERT_RETURNING)) {
// delimite primary key fields
if(is_array($this->getPrimary())) {
$primaryColumns = array();
foreach($this->getPrimary() as $primaryColumn) {
$primaryColumns[] = $driver->delimite($primaryColumn);
}
} else {
$primaryColumns = array($driver->delimite($this->getPrimary()));
}
// run insert query with RETURNING clause
$return = $this->connection->query($this->sqlBuilder->buildInsertQuery(), $data, ' RETURNING '.implode(',', $primaryColumns));
// fetch newly created primary key fields
$primary = $return->fetch();
$this->loadRefCache();
if ($data instanceof Nette\Database\SqlLiteral || $this->primary === NULL) {
unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
return $return->getRowCount();
}
if ($primary === FALSE) {
unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
return $return->getRowCount();
}
// fetch primary key fields
$primaryKey = array();
foreach($primary as $pColumn => $pValue) {
$primaryKey[$pColumn] = $pValue;
}
}
// if primary key contains only one field, use its value
if (count($primaryKey) === 1) {
$primaryKey = reset($primaryKey);
}
$row = $this->createSelectionInstance()
->select('*')
->wherePrimary($primaryKey)
->fetch();
if ($this->rows !== NULL) {
if ($signature = $row->getSignature(FALSE)) {
$this->rows[$signature] = $row;
$this->data[$signature] = $row;
} else {
$this->rows[] = $row;
$this->data[] = $row;
}
}
return $row;
}
Pak je potřeba upravit u jednotlivých driverů, zda podporují lastInsertId() či nikoliv, např. u MySQL ano:
/**
* @return bool
*/
public function isSupported($item)
{
return $item === self::SUPPORT_SUBSELECT || $item === self::SUPPORT_LAST_INSERT_ID;
}
A nakonec přikládám FirebirdDriver, který používám. Firebird PDO nepodporuje meta informace v metodě getColumnTypes, vyhodí výjimku „SQLSTATE[IM001]: Driver does not support this function: driver doesn't support meta data“, takže tam jsou dvě možnosti, buď to nepoužívat a připravit se o přetypování vytažených dat z databáze, nebo cesta, kterou je driver tvořen a sice (a teď mne nekamenujte :-)) vytvoření supportConnection přes ibase extension, kterou lze spustit SQL dotaz znovu (není-li to insert) a meta data vytáhnout:
- chloris
- Člen | 23
/**
* Nette Database Firebird Driver
*
* Example configuration:
*
* nette:
* database:
* dsn: 'firebird:dbname=[HOST]:[PATH];charset=utf-8'
* user: SYSDBA
* password: masterkey
* options:
* lazy: yes
* supportConnection:
* database: '[HOST]:[PATH]'
* user: SYSDBA
* password: masterkey
*
* supportConnection parameters needs to be equivalent to dsn/user/password parameters
*/
namespace Nette\Database\Drivers;
use Nette;
/**
* Supplemental Firebird database driver.
*
* @see http://www.php.net/manual/en/ref.pdo-firebird.connection.php
*/
class FirebirdDriver extends Nette\Object implements Nette\Database\ISupplementalDriver
{
/** @var Nette\Database\Connection */
private $connection;
/** Support connection to firebird database for obtaining META information */
private $supportConnection = null;
private $supportConnectionOptions = null;
/**
* Constructor
*/
public function __construct(Nette\Database\Connection $connection, array $options)
{
$this->connection = $connection;
// Check if we have configured the supportConnection
if(empty($options['supportConnection']) || empty($options['supportConnection']['user']) || !isset($options['supportConnection']['password'])) {
throw new Nette\InvalidArgumentException('Database connection "options" must contain "supportConnection" with subsequent keys "database", "user" and "password".');
}
$this->supportConnectionOptions = $options['supportConnection'];
}
/********************* SQL *******************/
/**
* Delimites identifier for use in a SQL statement.
*/
public function delimite($name)
{
// @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
return '"' . str_replace('"', '""', strtoupper($name)). '"';
}
/**
* Formats boolean for use in a SQL statement.
*/
public function formatBool($value)
{
return $value ? '1' : '0';
}
/**
* Formats date-time for use in a SQL statement.
*/
public function formatDateTime(/*\DateTimeInterface*/ $value)
{
return $value->format("'Y-m-d H:i:s'");
}
/**
* Encodes string for use in a LIKE statement.
*/
public function formatLike($value, $pos)
{
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
}
/**
* Injects LIMIT/OFFSET to the SQL query.
*/
public function applyLimit(&$sql, $limit, $offset)
{
if ($limit >= 0 || $offset > 0) {
$sql = preg_replace('#^\s*(SELECT|UPDATE|DELETE)#i',
'$0 FIRST '.($limit < 0 ? '18446744073709551615' : (int) $limit).($offset > 0 ? ' SKIP ' . (int) $offset : ''),
$sql, 1, $count);
if (!$count) {
throw new Nette\InvalidArgumentException('SQL query must begin with SELECT, UPDATE or DELETE command.');
}
}
}
/**
* Normalizes result row.
*/
public function normalizeRow($row)
{
return $row;
}
/********************* reflection *******************/
/**
* Returns list of tables.
*/
public function getTables()
{
$tables = array();
foreach ($this->connection->query('SELECT RDB$RELATION_NAME NAME, (CASE COALESCE(RDB$VIEW_SOURCE,0) WHEN 0 THEN 0 ELSE 1 END) IS_VIEW FROM RDB$RELATIONS') as $row) {
$tables[] = array(
'name' => trim($row['NAME']),
'view' => !empty($row['IS_VIEW']),
);
}
return $tables;
}
/**
* Returns metadata for all columns in a table.
*/
public function getColumns($table)
{
$columns = array();
$query =
'SELECT '.
'rf.RDB$RELATION_NAME AS relationname, '.
'rf.RDB$FIELD_NAME AS fieldname, '.
't.RDB$TYPE_NAME AS typename, '.
'f.RDB$FIELD_LENGTH AS fieldlength, '.
'f.RDB$DEFAULT_VALUE AS defaultvalue, '.
'rf.RDB$NULL_FLAG AS nullflag, '.
'(SELECT COUNT(*) '.
'FROM RDB$RELATION_CONSTRAINTS rc '.
'JOIN RDB$INDEX_SEGMENTS ins ON rc.RDB$INDEX_NAME = ins.RDB$INDEX_NAME '.
'WHERE rc.RDB$CONSTRAINT_TYPE = \'PRIMARY KEY\' '.
'AND rc.RDB$RELATION_NAME = rf.RDB$RELATION_NAME '.
'AND ins.RDB$FIELD_NAME = rf.RDB$FIELD_NAME '.
') AS isprimary '.
'FROM RDB$RELATION_FIELDS rf '.
'JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE '.
'JOIN RDB$TYPES t ON f.RDB$FIELD_TYPE = t.RDB$TYPE AND t.RDB$FIELD_NAME = \'RDB$FIELD_TYPE\' '.
'WHERE rf.RDB$RELATION_NAME = \''.$table.'\'';
foreach ($this->connection->query($query) as $row) {
//$type = explode('(', $row['Type']);
$columns[] = array(
'name' => trim($row->FIELDNAME),
'table' => $table,
'nativetype' => strtoupper(trim($row->TYPENAME)),
'size' => $row->FIELDLENGTH,
'unsigned' => FALSE, //(bool) strstr($row['Type'], 'unsigned'),
'nullable' => !empty($row->NULLFLAG),
'default' => $row->DEFAULTVALUE,
'autoincrement' => FALSE, //$row['Extra'] === 'auto_increment',
'primary' => (bool)$row->ISPRIMARY,
'vendor' => (array) $row,
);
}
return $columns;
}
/**
* Returns metadata for all indexes in a table.
*
* @param string $table Table name
* @return array array of indices
*/
public function getIndexes($table)
{
$query =
'SELECT '.
'ixs.RDB$FIELD_POSITION AS FIELD_POSITION, '.
'ix.RDB$INDEX_NAME AS INDEX_NAME, '.
'ix.RDB$UNIQUE_FLAG AS UNIQUE_FLAG, '.
'ixs.RDB$FIELD_NAME AS FIELD_NAME, '.
'CASE COALESCE(rc.RDB$CONSTRAINT_TYPE,\'\') WHEN \'PRIMARY KEY\' THEN 1 ELSE 0 END AS IS_PRIMARY '.
'FROM RDB$INDICES ix '.
'LEFT JOIN RDB$INDEX_SEGMENTS ixs ON ix.RDB$INDEX_NAME = ixs.RDB$INDEX_NAME '.
'LEFT JOIN RDB$RELATION_CONSTRAINTS rc ON ix.RDB$RELATION_NAME = rc.RDB$RELATION_NAME AND ix.RDB$INDEX_NAME = rc.RDB$INDEX_NAME '.
'WHERE ix.RDB$RELATION_NAME = \''.$table.'\'';
$indexes = array();
foreach ($this->connection->query($query) as $row) {
$ixName = trim($row['INDEX_NAME']);
$indexes[$ixName]['name'] = $ixName;
$indexes[$ixName]['unique'] = (bool)$row['UNIQUE_FLAG'];
$indexes[$ixName]['primary'] = (bool)$row['IS_PRIMARY'];
$indexes[$ixName]['columns'][$row['FIELD_POSITION']] = $row['FIELD_NAME'];
}
return array_values($indexes);
}
/**
* Returns metadata for all foreign keys in a table.
*/
public function getForeignKeys($table)
{
$query =
'SELECT '.
'rc.RDB$CONSTRAINT_NAME AS constraint_name, '.
'i.RDB$RELATION_NAME AS table_name, '.
's.RDB$FIELD_NAME AS field_name, '.
'i2.RDB$RELATION_NAME AS references_table, '.
's2.RDB$FIELD_NAME AS references_field '.
'FROM RDB$INDEX_SEGMENTS s '.
'LEFT JOIN RDB$INDICES i ON i.RDB$INDEX_NAME = s.RDB$INDEX_NAME '.
'LEFT JOIN RDB$RELATION_CONSTRAINTS rc ON rc.RDB$INDEX_NAME = s.RDB$INDEX_NAME '.
'LEFT JOIN RDB$REF_CONSTRAINTS refc ON rc.RDB$CONSTRAINT_NAME = refc.RDB$CONSTRAINT_NAME '.
'LEFT JOIN RDB$RELATION_CONSTRAINTS rc2 ON rc2.RDB$CONSTRAINT_NAME = refc.RDB$CONST_NAME_UQ '.
'LEFT JOIN RDB$INDICES i2 ON i2.RDB$INDEX_NAME = rc2.RDB$INDEX_NAME '.
'LEFT JOIN RDB$INDEX_SEGMENTS s2 ON i2.RDB$INDEX_NAME = s2.RDB$INDEX_NAME '.
'WHERE rc.RDB$CONSTRAINT_TYPE = \'FOREIGN KEY\' AND rc.RDB$RELATION_NAME = \''.$table.'\' '.
'ORDER BY s.RDB$FIELD_POSITION';
$keys = array();
foreach ($this->connection->query($query) as $id => $row) {
$keys[$id]['name'] = trim($row['CONSTRAINT_NAME']); // foreign key name
$keys[$id]['local'] = trim($row['FIELD_NAME']); // local columns
$keys[$id]['table'] = trim($row['REFERENCES_TABLE']); // referenced table
$keys[$id]['foreign'] = trim($row['REFERENCES_FIELD']); // referenced columns
}
return array_values($keys);
}
/**
* Returns associative array of detected types (IReflection::FIELD_*) in result set.
*/
public function getColumnTypes(\PDOStatement $statement)
{
$types = array();
$count = $statement->columnCount();
// it's not a good idea to run insert two-times !
if(Nette\Utils\Strings::contains(strtoupper($statement->queryString), 'INSERT INTO'))
{
// SQLSTATE[IM001]: Driver does not support this function: driver doesn't support meta data
}
else
{
// run the same query on support connection which supports meta data
if(empty($this->supportConnection)) {
$this->supportConnection = ibase_connect(
$this->supportConnectionOptions['database'],
$this->supportConnectionOptions['user'],
$this->supportConnectionOptions['password']
);
}
$res = ibase_query($this->supportConnection, $statement->queryString);
// for each column get metadata
for ($col = 0; $col < $count; $col++) {
$meta = $this->getColumnMeta($res, $col);
if (isset($meta['native_type'])) {
$types[$meta['name']] = $type = Nette\Database\Helpers::detectType($meta['native_type']);
if ($type === Nette\Database\IReflection::FIELD_TIME) {
$types[$meta['name']] = Nette\Database\IReflection::FIELD_TIME_INTERVAL;
}
}
}
}
return $types;
}
/**
* @return bool
*/
public function isSupported($item)
{
return $item === self::SUPPORT_SELECT_UNGROUPED_COLUMNS || $item === self::SUPPORT_MULTI_COLUMN_AS_OR_COND || $item == self::SUPPORT_INSERT_RETURNING;
}
/**
* Reads metadata of given resultset and columnIndex
* @param mixed $res Result from helper connection
* @param mixed $col Result column index
* @return array
*/
public function getColumnMeta($res, $col)
{
$colInfo = ibase_field_info($res, $col);
return array(
'native_type' => $colInfo['type'],
'name' => empty($colInfo['alias']) ? $colInfo['name'] : $colInfo['alias'],
'len' => $colInfo['length'],
);
}
/**
* Disconnect helper databse connection
*/
public function __destruct()
{
if(!empty($this->supportConnection))
{
ibase_close($this->supportConnection);
}
}
}
- chloris
- Člen | 23
Tak vzhledem k mně neznámému této podmínky:
https://api.nette.org/…sor.php.html#111
108: private function formatValue($value)
109: {
110: if (is_string($value)) {
111: if (strlen($value) > 20) { // proč 20? co to je a komu to slouží, druhé straně?
112: $this->remaining[] = $value;
113: return '?';
114:
115: } else {
116: return $this->connection->quote($value);
117: }
nefunguje vždy supportConnection.
- verex
- Člen | 12
<?php
public function delimite($name)
{
// @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
return '"' . str_replace('"', '""', strtoupper($name)). '"';
}
?>
Proč je tam použito strtoupper(…)?
Kdyz totiz mam sloupec treba nazevSloupce a dam ho do uvozovek, firebird bere
nazeb sloupce case sensitive a ten strtoupper mi to jaksi porusi. Radeji bych
fci delimite videl takto:
<?php
public function delimite($name)
{
// @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
return '"' . str_replace('"', '""', $name). '"';
}
?>
- chloris
- Člen | 23
Je to tak. „strtoupper“ tam být nemá. Žil jsem v domnění, že Firebird má vše povinně v uppercase. Nicméně tento driver do Nette Database nebude fungovat vždy na 100%, protože Nette Database jako taková funguje nad PDO, které má experimental podporu pro Firebird. Takže se často stává, že aplikace volající např. proceduru „spadne“ na segfault bez chyby v logu. Řešením je opustit PDO a používat ibase extension pro PHP a tu naroubovat na Nette Database. Připravil jsem pro to extension, dám jí v nejbližších dnech do addons. Takže pak doporučuju pro větší stabilitu používat ji.
- verex
- Člen | 12
Aha, to zní lákavě.
Aktuálně mi driver dostačuje. Potřebuji jej jen na čtení dat z několika
pohledů, přičemž pouze nad jedním se zatím provádí update. Replikuju si
totiž data z FB databáze do MySQL pro webovou aplikaci. S tou opravou mi
driver dostačuje.
Narazil jsem teda ještě na jedno úskalí: Firebird má striktně typ DATE a TIME zvlášť a když se pro něj formátuje zápis DATE naformátuje se včetně času a to se Firebirdu nelíbí – neumí tu hodnotu jako DATE použít.