Doctrine DBAL Insert if doesn't exist - php

I'm using Symfony2-framework with Doctrine DBAL, and I'm inserting some data into MySql-database. Insert looks something like this(simplified):
$conn->insert('sometable', array(
"col1" => $data1,
"col2" => $data2,
"col3" => $data3
));
What I would like to achieve is the functionality like the ordinary sql-has. The ability to insert if doesn't exist, like: INSERT IGNORE. But is it possible to do such a thing with DBAL?
Note that I don't use objects here.
Edit: Please do note that I'm not using objects, but rather the depicted array-insert-method of DBAL.
Edit2: I tried to approach the problem with using the suggested try-catch, which seems to work quite well except for one thing. The db auto increments the primary key even if no new rows were added.
Here is the code that I used:
try{
$conn->insert('sometable', array(
"col1" => $data1,
"col2" => $data2,
"col3" => $data3
));
} catch( \Exception $e) {
switch (get_class($e)) {
case 'Doctrine\DBAL\DBALException':
// No problems here. Just means that the row already existed.
break;
default:
$this->get('logger')->error("(ERROR in ".__METHOD__.", #Row: ".(__LINE__)."): DB-error! error: ".$e->getMessage());
break;
}
}
And I also had to do a multiple row unique index for the table, because I have to check if all the columns are the same. i.e. if the whole row is the same as the one we are trying to insert.
So.. It works well otherwise, except that the auto increment value keeps rising up every time we try-insert-catch. I don't think it's a real problem, but it just feels stupid to waste numbers.. :D

I'm not aware of a way to do this other than just doing a select query and try to retrieve the row, i.e:
$sql = "SELECT * FROM sometable WHERE ....";
$stmt = $conn->prepare($sql);
$stmt->bindParam(); //or bindValue...
$stmt->execute();
$exists = $stmt->fetch();
if(!$exists) {
// insert or whatever..
} else {
// do nothing?
}
Eventually you might be able to attach Event which would do that for you, but I'm not sure if that applies in your case.
Alternatively you could create an unique constraint and do a try/catch when inserting your data. Whenever the data is not unique, the database would return an error and you'd usually receive a PDOException although in Doctrine I think it's with a different name (check exceptions). Just catch that exception and do nothing with it. Example:
try {
$conn->insert('sometable', array(
"col1" => $data1,
"col2" => $data2,
"col3" => $data3
));
} catch(PDOException $e) {
// do nothing.
}

Related

Update a row using idiorm and php

I have this function to update a record, but i cannot it fails and send me a "Primary key ID missing from row or is null" message, how can I fix it?
public static function update_child($data)
{
try
{
$update= ORM::for_table("dm_child",DM_TAG)
->where_equal($data["id_child"]);
$update -> set(array(
"gender" => $data["gender"]
"age_year" =>$data["year"]
"age_month" => $data["month"]
));
$update -> save();
}
catch(PDOException $ex)
{
ORM::get_db()->rollBack();
throw $ex;
}
}
Idiorm assumes that the name of the primary key is 'id', which is not that, in your case.
Therefore you have to explicitly specify it to Idiorm:
<?php
ORM::configure('id_column_overrides', array(
'dm_child' => 'id_child',
'other_table' => 'id_table',
));
See Docs>Configuration.
The answer is indeed the one provided by #iNpwd for changing the default 'id' column name for queries on a per table basis:
ORM::configure('id_column_overrides', array(
'table_name' => 'column_name_used_as_id',
'other_table' => array('pk_1', 'pk_2') // a compound primary key
));
The thing that was biting me on getting it to recognize my query was WHERE I was changing the ORM::configure values. I was not in the correct file.
A deeper link to specifically the ID Column configuration: http://idiorm.readthedocs.org/en/latest/configuration.html#id-column
I just met this problem 2 minutes ago. The real reason is, you forgot select id field in querying.
demo:
$demo = ORM::for_table('demo')->select('field_test')->find_one($id);
$demo->field_test = 'do';
$demo->save();
You will get the error.
change to :
$demo = ORM::for_table('demo')->select('field_test')->select('id')->find_one($id);
It will fix the problem.
Some tips in documents:
https://github.com/j4mie/idiorm/blob/master/test/ORMTest.php
/**
* These next two tests are needed because if you have select()ed some fields,
* but not the primary key, then the primary key is not available for the
* update/delete query - see issue #203.
* We need to change the primary key here to something other than id
* becuase MockPDOStatement->fetch() always returns an id.
*/
I've never used idiorm, so cannot guarantee that my answer will work for you, but from this page and under "Updating records", we have an example which is similar but slightly different to yours.
// The 5 means the value of 5 in the primary-key column
$person = ORM::for_table('person')->find_one(5);
// The following two forms are equivalent
$person->set('name', 'Bob Smith');
$person->age = 20;
// This is equivalent to the above two assignments
$person->set(array(
'name' => 'Bob Smith',
'age' => 20
));
// Syncronise the object with the database
$person->save();
I'm sure I'll learn the reason behind this, but let me tell you all I understand at the moment, and how I "fixed" it.
Here is the beginning of idiorm's save function:
public function save() {
$query = array();
// remove any expression fields as they are already baked into the query
$values = array_values(array_diff_key($this->_dirty_fields, $this->_expr_fields));
if (!$this->_is_new) { // UPDATE
// If there are no dirty values, do nothing
if (empty($values) && empty($this->_expr_fields)) {
return true;
}
$query = $this->_build_update();
$id = $this->id(true);
Right there, on that last line, when trying to access the $this->id, you are getting an exception thrown:
throw new Exception('Primary key ID missing from row or is null');
$this does not contain an id property. I'm not really sure how it could. The example given both on their homepage and in the docs doesn't do anything special to address this. In fact I am copying them 1:1 and still yielding the same error as you.
So, all that said, I fixed this error by just adding in my own id:
$crop = ORM::for_table('SCS_Crop')->find_one($id);
$crop->id = $id;
$crop->Name = "Foo";
$crop->save();
This also happens when the id field name is ambiguous, e.g. when joining two tables both having an id column. This is the case with referenced tables
Model::factory('tableOne')
->left_outer_join('tableTwo', array('tableOne.tableTwo_id', '=', 'tableTwo.id'))
->find_one($id);
In these cases set an alias to the ID column of the parent tableOne to later access it while saving. Make sure that you also select other columns you need - e.g. by ->select('*'):
Model::factory('tableOne')
->select('*')
->select('tableOne.id', 'id')
->left_outer_join('tableTwo', array('tableOne.tableTwo_id', '=', 'tableTwo.id'))
->find_one($id);
if in table primary key/ field name not id then following id column overrides required
default id (primary_key) to replace with other id name (primary_key)
ORM::configure('id_column_overrides', array(
'user' => 'user_id',
));
$update = ORM::for_table('user')->find_one(1);
$update->name = "dev";
try{
$update->save();
}catch(Exception $e){
echo $e;
}
print_r($update);

How do I reduce repetitive code when inserting the same metadata into identical columns over multiple tables?

My goals:
Follow database-normalization.
Ability to track changes in my tables.
Ability to restore the state of my database from a given
point in time, e.g. last month.
Separate the code that is processing the data so that the
input can come either from a HTML-form or a script in another language (Ruby/perl).
To accomplish this, I've opted for a database design like the one described in this answer:
StackOverflow: Is there a MySQL option/feature to track history of changes to records?
However, when a user updates several fields, the same metadata has to be inserted into multiple tables that contain identical columns, and my code becomes repetitive.
Example:
A user submits data through a HTML-form.
PHP processes the data like below, with the help of Propel ORM.
function insertEvent($name, $venueId, $user, $date, $id = 0) {
//validation and formatting..
//if id == 0, new event,
//else, this is an update, archive the old database row on successful processing and maintain a common id as a parent
//...
$event = new Event();
$event->setName($name);
$event->setVenueId($venueId);
$event->setUser($user); //1
$event->setValidFrom($date); //1
$event->setValidUntil(null); //1
$event->save();
// ...
}
function insertEventPhonenumber($phonenumber, $pid, $user, $date, $id = 0) {
//...
$event_phonenumber = new EventPhonenumber();
$event_phonenumber->setPid($pid); //2
$event_phonenumber->setPhonenumber($phonenumber);
$event_phonenumber->setUser($user); //2
$event_phonenumber->setValidFrom($date); //2
$event_phonenumber->setValidUntil(null); //2
$event_phonenumber->save();
// ...
}
function insertEventArtistId($artistId, $pid, $user, $date, $id = 0) {
//...
$event_artistId = new EventArtistId();
$event_artistId->setPid($pid); //3
$event_artistId->setArtistId($artistId);
$event_artistId->setUser($user); //3
$event_artistId->setValidFrom($date); //3
$event_artistId->setValidUntil(null); //3
$event_artistId->save();
// ...
}
My problem:
In my full code there are more tables affected than the three in the example.
Marked with //1, //2 and //3, you see data input that is often going to be identical.
In my stomach, I don't like this. I've been trying search engines with queries like 'common columns in SQL insert queries over multiple tables' and variations of the wording, without finding anything directly related to my problem.
Is this bad practice like it feels to me?
How can I minimize the repetition in my code?
Are you trying to get rid of all of those functions? If so you can use the call_user_func to eliminate most of your code.
<?php
class MyApp {
static function insert($name, $params) {
$ob = new $name();
foreach($params as $k=>$v) {
$op = array($ob,"set".ucfirst($k));
//if (is_callable($op)) {
call_user_func($op,$v);
//}
}
$ob->save();
}
}
MyApp::insert(
"EventArtistId",
array(
"pid"=>$_REQUEST['pid'],
"artistId"=>$_REQUEST['artistId'],
"user" => $_REQUEST['user'],
"validFrom" => $_REQUEST['date'], // may need to convert date
"validUntil" => isset($_REQUEST['validUntil']) ? $_REQUEST['validUntil'] : null,
)
);
// More cool
// MyApp::insert($_REQUEST['action'],$_REQUEST['params']);
?>

MongoDB PHP: Insert if not exists equivalent

I use MongoDB together with PHP. To create a unique document for each user, I set the username to unique with
$db->ensureIndex(array("username" => 1), array("unique" => 1, "dropDups" => 1));
However, if there is a duplicate, the _id value that is being returned for the user is that of the record which got never created and not that of the record which already exists in the db.
Here's the entire function:
function register_user($username, $password, $first_name, $last_name, $email)
{
$m = new Mongo();
$db = $m->users->details;
$db->ensureIndex(array("username" => 1), array("unique" => 1, "dropDups" => 1));
$user_doc = array( "username" => $username, "password" => $password, "first_name" => $first_name, "last_name" => $last_name, "email" => $email);
$db->insert($user_doc);
return $user_doc["_id"]; // here i return the _id
}
How would I go about creating a user database, that has no duplicate usernames?
EDIT:
Here's my temporary solution. I put it right where the insert used to be.
try
{
$db->insert($user_doc,array('safe'=>1));
}
catch ( MongoCursorException $e )
{
// if this did not work, we already have that user inside and want to get his _id
$user = $db->findOne(array('username' => $username, 'password' => $password));
return $user["_id"];
}
If I try to insert it with the safe option, it would always die. Using the try-catch procedure, I try to insert and if it does not work, manually find the _id by username. I'm still not satisfied with this solution, so it would be great if more proficient MongoDB users could give some advice.
First off, you always need that try...catch. The reason you were getting the wrong _id for $user_doc is simply that the driver creates it before sending it to the database. So even if the insert fails it's still on the document.
How would I go about creating a user database, that has no duplicate usernames?
I would start by over-writing the _id field with the user name. The _id is auto-generated as a convenience, but you can set it yourself before you insert.
You will still need to use the try...catch but you will get a specific duplicate key error. You can then use this to correctly handle the case of "that user name already exists".
Here's how I manage to insert if not exists.
$db->update(
array('email' => $email),
array('$setOnInsert' => array('email' => $email)),
array('upsert' => true)
);
This basically just updates when the record exists ("updating" to what it's already set to and $setOnInsert does nothing), but if it doesn't exist, mongo upserts a new record then creates the new key and assigns the value.

Duplicate key error during upsert [Explanation]

I am executing the below statement in a class. This code is from
$query = array('_id' => $id, 'lock' => 0);
$update = array('$set' => array('lock' => 1));
$options = array('safe' => true, 'upsert' => true);
$result = $this->_mongo->update($query, $update, $options);
if ($result['ok'] == 1) {
return true;
}
However I do not understand how I would get a duplicate key error.
Can someone explain the possible scenarios and likelihood that I will receive this error?
I have been researching this extensively, cannot find my answer anywhere. So if it is on SO or any other website please share!
Thanks in advance.
Since you're doing an upsert and including _id in your query, you shouldn't be getting any duplicates on that key. This makes me think that you've created a unique index on lock, which isn't going to work for more than 2 documents because you only have 2 values for that field.
If you haven't put a unique index on lock, then you must have a unique index on a field you aren't showing here. That won't work either because on an insert, your upsert is going to set _id and lock only, any other field with an index will be inserted as null. If one of those fields has a unique index, then only a single document can have a null in that field. So when you try and insert another null for that field, you'll get a duplicate key error.

Database transactions in Zend Framework: Are they isolated?

Using Zend Framework, I need to (1) read a record from a MySQL database, and (2) immediately write back to that record to indicate that it has been read. I don't want other processes or queries to be able to read from or write to the same record in between steps (1) and (2).
I was considering using a transaction for these steps. If I use the following methods, will that fulfil my requirements?
Zend_Db_Adapter_Abstract::beginTransaction()
Zend_Db_Adapter_Abstract::commit()
Zend_Db_Adapter_Abstract::rollBack()
Presupposing you are using the InnoDB engine for tables that you will issue transactions on:
If the requirement is that you first need to read the row and exclusively lock it, before you are going to update it, you should issue a SELECT ... FOR UPDATE query. Something like:
$db->beginTransaction();
try
{
$select = $db->select()
->forUpdate() // <-- here's the magic
->from(
array( 'a' => 'yourTable' ),
array( 'your', 'column', 'names' )
)
->where( 'someColumn = ?', $whatever );
$result = $this->_adapter->fetchRow( $select );
/*
alter data in $result
and update if necessary:
*/
$db->update( 'yourTable', $result, array( 'someColumn = ?' => $whatever ) );
$db->commit();
}
catch( Exception $e )
{
$db->rollBack();
}
Or simply issue 'raw' SELECT ... FOR UPDATE and UPDATE SQL statements on $db of course.

Categories