Do I need a transaction here? - php

I have a web application in which users create objects that get stored in a MySQL database. Each object has a globally unique identifier that is stored in a table.
DROP TABLE IF EXISTS `uidlist`;
CREATE TABLE IF NOT EXISTS `uidlist` (
`uid` varchar(9) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
`chcs` varchar(16) DEFAULT NULL,
UNIQUE KEY `uid` (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=ascii;
When a new object is to be created and stored I generate a new uid and ensure that it does not already exist in the uidlist table. (I should mention that collisions are rare since the potential range of UIDs I have is very large).
No issues here - it works just fine. However, with an increasing number of users wanting to simultaneously create + store objects the need to check uidlist is liable to become a bottleneck.
To circumvent the problem here is what I have done:
I have a secondary table
DROP TABLE IF EXISTS `uidbank`;
CREATE TABLE IF NOT EXISTS `uidbank` (
`uid` varchar(9) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
`used` tinyint(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=ascii;
I pre-populate this table at short intervals via a CRON job - which ensures that it always has 1000 uid values that are tested for uniqueness.
When a live user requires a new UID I do the following:
function makeUID()
{
global $dbh;
$sql = "DELETE FROM `uidbank` WHERE used = '1';";
//discard all "used" uids from previous hits on the bank
$sql .= "UPDATE `uidbank` SET used = '1' WHERE used = '0' LIMIT 1;";
//setup a new hit
$dbh->exec($sql);
//be done with that
$sql = "SELECT uid FROM `uidbank` WHERE used = '1'";
$uid = $dbh->query($sql)->fetchColumn();
//now pickup the uid hit we just setup
return $uid;
//return the "safe" uid ready for use
}
No issues here either. It works perfectly well in my single user test environment. However, my SQL skills are pretty basic so I am not 100% sure that
this is the right way to handle the job
that my "safe" UID pickup method will not return unsafe values because in the mean time another user has been assigned the same UID.
I'd much appreciate any tips on how this scheme might be improved.

Any reason you are not using a serial as your unique identifier? That would definitely be my first suggestion and would negate the necessity for the complicated setup you have.
Assuming that there is some reason then the biggest flaw I can see in your current makeUID call is that you are likely to get into a situation whereby the update sets 'used' to 1 and this row is deleted by a secondary call to makeUID before it has been able to successfully return the column meaning your second select (SELECT uid FROM uidbank WHERE used = '1') would return no rows
Could you explain why you dont use a serial so then I can try and get a better idea as to what is going on

Related

Unexpected double inserts with MyISAM

I have a like table for customers like products in my website.
The problem is, I use this table:
CREATE TABLE IF NOT EXISTS `likes` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user` varchar(40) NOT NULL,
`post` int(11) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
user who like, post is product id.
like button send a ajax request to this php:
session_start();
$user = $_SESSION["user"];
$pinsid=$_POST['id'];
$stmt = $mysqli_link->prepare("SELECT * FROM likes WHERE post=? AND user=?");
$stmt->bind_param('is', $pinsid, $user);
$stmt->execute();
$result = $stmt->get_result();
$stmt->close();
$chknum = $result->num_rows;
if($chknum==0){
$stmt = $mysqli_link->prepare("INSERT INTO likes (user, post) VALUES (?,?)");
$stmt->bind_param('si', $user, $pinsid);
$stmt->execute();
$stmt->close();
$response = 'success';
}
echo json_encode($response);
My problem is, I have double inserts in like from the same person. eg:
1 josh 5
2 josh 5
but it only happens if MySQL engine is set as InnoDB, if I change it to MyISAM I have only 1 insert.
What is happening? What should I do to make it work properly?
but it only happens if MySQL engine is set as InnoDB, if I change it to MyISAM I have only 1 insert.
What is happening? What should I do to make it work properly?
The MyISAM engine uses table level locking, which means that if an operation is executing on a table, all other operations wait executing till that oparation is finished.
InnoDb is transactional and uses row-level locking, since you're not using transactions nothing is locked.
As mentioned in the comments and answers the simplest solution is to create an unique constraint on user and post, in youre case you can use both as primary key because the auto-increment column has no added value.
To create a unique constraint:
ALTER TABLE likes ADD UNIQUE KEY uk_user_post (user,post);
As for your question:
but it can slow down my inserts?
If we speak solely about the insert operation at the table, yes it does slow down because each index has to be rebuild after an insert,update or delete operation. How much it slows down depends on the size of the index(es) and the number of rows in the table.
However in your current table structure you have no indexes at all on user and post, and in your application you perform a select with a lookup on both colums, which will result in a full table scan.
With the unique index (user,post) you can skip the select because when the unique constraint is violate you'll get an SQL error.
Also user and post are foreign keys so the should be indexed anyway.
The unique index (user,post) covers the user FK, so you will also need an index on post separatly
One way of doing this would be to set up a unique key for user and post in the likes table (see https://dev.mysql.com/doc/refman/5.0/en/constraint-primary-key.html).
If that was in place, the database would ensure that there are no duplicates of user and post. However, for data which are already in the table, it could be problematic if there are already duplicates

Logical Help Needed in a PHP Script

I am writing a little PHP script which is simply return data from a MYSQL table using below query
"SELECT * FROM data where status='0' limit 1";
After reading the data I update the status by getting Id of the particular row using below query
"Update data set status='1' WHERE id=" . $db_field['id'];
Things are working good for a single client. Now i am willing to make this particular page for multiple clients. There are more then 20 clients which will access the same page on almost same time continuously (24/7). Is there a possibility that two or more clients read same data from table? If yes then how to solve it?
Thanks
You are right to consider concurrency. Unless you have only 1 PHP thread responding to client requests, there's really nothing to stop them each from handing out the same row from data to be processed - in fact, since they will each run the same query, they'll each almost certainly hand out the same row.
The easiest way to solve that problem is locking, as suggested in the accepted answer. That may work if the time the PHP server thread takes to run the SELECT...FOR UPDATE or LOCK TABLE ... UNLOCK TABLES (non-transactional) is minimal, such that other threads can wait while each thread runs this code ( it's still wasteful, as they could be processing some other data row, but more on that later).
There is a better solution, though it requires a schema change. Imagine you have a table such as this:
CREATE TABLE `data` (
`data_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`data` blob,
`status` tinyint(1) DEFAULT '0',
PRIMARY KEY (`data_id`)
) ENGINE=InnoDB;
You don't have any way to transactionally update "the next processed record" because the only field you have to update is status. But imagine your table looks more like this:
CREATE TABLE `data` (
`data_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`data` blob,
`status` tinyint(1) DEFAULT '0',
`processing_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`data_id`)
) ENGINE=InnoDB;
Then you can write a query something like this to update the "next" column to be processed with your 'processing id':
UPDATE data
SET processing_id = #unique_processing_id
WHERE processing_id IS NULL and status = 0 LIMIT 1;
And any SQL engine worth a darn will make sure you don't have 2 distinct processing IDs accounting for the same record to be processed at the same time. Then at your leisure, you can
SELECT * FROM data WHERE processing_id = #unique_processing_id;
and know that you're getting a unique record every time.
This approach also lends it well to durability concerns; you're basically identify the batch processing run per data row, meaning you can account for each batch job whereas before you're potentially only accounting for the data rows.
I would probably implement the #unique_processing_id by adding a second table for this metadata ( the auto-increment key is the real trick to this, but other data processing metadata could be added):
CREATE TABLE `data_processing` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`data_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
and using that as a source for your unique IDs, you might end up with something like:
INSERT INTO data_processing SET date=NOW();
SET #unique_processing_id = (SELECT LAST_INSERT_ID());
UPDATE data
SET processing_id = #unique_processing_id
WHERE status = 0 LIMIT 1;
UPDATE data
JOIN data_processing ON data_processing.id = data.processing_id
SET data_processing.data_id = data.data_id;
SELECT * from data WHERE processing_id = #unique_processing_id;
-- you are now ready to marshal the data to the client ... and ...
UPDATE data SET status = 1
WHERE status = 0
AND processing_id = #unique_processing_id
LIMIT 1;
Thus solving your concurrency problem, and putting you in better shape to audit for durability as well, depending on how you set up data_processing table; you could track thread IDs, processing state, etc. to help verify that the data is really done being processed.
There are other solutions - a message queue might be ideal, allowing you to queue each unprocessed data object's ID to the clients directly ( or through a php script ) and then provide an interface for that data to be retrieved and marked processed separately from the queueing of the "next" data. But as far as "mysql-only" solutions go, the concepts behind what I've shown you here should server you pretty well.
The answer you seek might be using transactions. I suggest you read the following post and its accepted answer:
PHP + MySQL transactions examples
If not, there is also table locking you should look at:
13.3.5 LOCK TABLES and UNLOCK TABLES
I will suggest you to use session for this...
you can save that id into session...
so you can check if one client is checking that record, than you can not allow another client to access it ...

MySQL conditional user permissions

The question… Is it possible to add MySQL permissions to only allow to select fields based on permissions?
Example:
User user1 can only select/insert/delete from the users table where the column instance is equal to 1 (1 being passed via PHP).
User user2 can only select/insert/delete from the users table where the column instance is equal to 2 (1 being passed via PHP).
Here's the background info:
I'm creating an application with the same code base being used for multiple sites. Conditions within the app load different layouts. The multiple sites are using the same database because most information can be shared between sites. A user that registers on one site must also register on another site (this is how we want it because the sites are "by invitation only")
What I'm thinking of doing is to have users table: id, email, password, instance. The instance column would have the id of the site.
On the application layer every time I need to select/insert/delete from this table I append instance = [site_id] to the query... example: SELECT * FROM users WHERE email = '' AND instance = [site_id];
no, the permission is per table,db,server,etc but not for rows, however there is a solution, you can use view tables and set permission to user, for example
mysql> CREATE VIEW test.v AS SELECT * FROM t where email = '' AND instance = [site_id];
just create 2 view tables and grant access to those users
here is the Mysql documentation
It is not possible from what I know, MySQL doesn't allow conditional users.
Use one user for both sites and modify your all queries accordingly to your 'instance'. So every time you query something site-specific you add WHERE instance = $site_id.
MySQL does not facilitate using permissions to lock down certain rows based on the MySQL user that is connected to the database.
If you have users that you want to limit in such a way, it is probably best to not give them direct database access, but have them connecting through another layer.
Using a view is not a good idea - every user would have to use different queries (referencing their own personal view) to accomplish the same things. If you were just limiting the columns that a user could see (instead of the rows), a view would be a good solution.
It is possible to use stored procedures to accomplish something like what you're looking for:
# This table already exists in your schema
CREATE TABLE `user` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`email` VARCHAR(50) NOT NULL,
`instance` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB;
INSERT INTO `user`(`id`,`email`,`instance`)
VALUES ( NULL,'wat#lol.com','1');
INSERT INTO `user`(`id`,`email`,`instance`)
VALUES ( NULL,'huh#bwu.com','2');
INSERT INTO `user`(`id`,`email`,`instance`)
VALUES ( NULL,'no#yes.lol','1');
# This would be a new table you would have to create
CREATE TABLE `user_instance` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`user` VARCHAR(20) NOT NULL,
`instance` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_instance` (`user`,`instance`)
) ENGINE=INNODB;
INSERT INTO `user_instance`(`user`,`instance`) VALUES ('user1','1');
INSERT INTO `user_instance`(`user`,`instance`) VALUES ('user2','2');
# This is a stored procedure that might accomplish what you want
DELIMITER $$
CREATE PROCEDURE `p_get_users`()
BEGIN
SELECT `user`.`id`, `user`.`email`
FROM `user`
WHERE `user`.`instance` IN(
SELECT `instance`
FROM `user_instance`
WHERE `user` = USER());
END$$
DELIMITER ;

How to get next alpha-numeric ID based on existing value from MySQL

First, I apologize if this has been asked before - indeed I'm sure it has, but I can't find it/can't work out what to search for to find it.
I need to generate unique quick reference id's, based on a company name. So for example:
Company Name Reference
Smiths Joinery smit0001
Smith and Jones Consulting smit0002
Smithsons Carpets smit0003
These will all be stored in a varchar column in a MySQL table. The data will be collected, escaped and inserted like 'HTML -> PHP -> MySQL'. The ID's should be in the format depicted above, four letters, then four numerics (initially at least - when I reach smit9999 it will just spill over into 5 digits).
I can deal with generating the 4 letters from the company name, I will simply step through the name until I have collected 4 alpha characters, and strtolower() it - but then I need to get the next available number.
What is the best/easiest way to do this, so that the possibility of duplicates is eliminated?
At the moment I'm thinking:
$fourLetters = 'smit';
$query = "SELECT `company_ref`
FROM `companies`
WHERE
`company_ref` LIKE '$fourLetters%'
ORDER BY `company_ref` DESC
LIMIT 1";
$last = mysqli_fetch_assoc(mysqli_query($link, $query));
$newNum = ((int) ltrim(substr($last['company_ref'],4),'0')) + 1;
$newRef = $fourLetters.str_pad($newNum, 4, '0', STR_PAD_LEFT);
But I can see this causing a problem if two users try to enter company names that would result in the same ID at the same time. I will be using a unique index on the column, so it would not result in duplicates in the database, but it will still cause a problem.
Can anyone think of a way to have MySQL work this out for me when I do the insert, rather than calculating it in PHP beforehand?
Note that actual code will be OO and will handle errors etc - I'm just looking for thoughts on whether there is a better way to do this specific task, it's more about the SQL than anything else.
EDIT
I think that #EmmanuelN's suggestion of using a MySQL trigger may be the way to handle this, but:
I am not good enough with MySQL, particularly triggers, to get this to work, and would like a step-by-step example of creating, adding and using a trigger.
I am still not sure whether this will will eliminate the possibility of two identical ID's being generated. See what happens if two rows are inserted at the same time that result in the trigger running simultaneously, and produce the same reference? Is there any way to lock the trigger (or a UDF) in such a way that it can only have one concurrent instance?.
Or I would be open to any other suggested approaches to this problem.
If you are using MyISAM, then you can create a compound primary key on a text field + auto increment field. MySQL will handle incrementing the number automatically. They are separate fields, but you can get the same effect.
CREATE TABLE example (
company_name varchar(100),
key_prefix char(4) not null,
key_increment int unsigned auto_increment,
primary key co_key (key_prefix,key_increment)
) ENGINE=MYISAM;
When you do an insert into the table, the key_increment field will increment based on the highest value based on key_prefix. So insert with key_prefix "smit" will start with 1 in key_inrement, key_prefix "jone" will start with 1 in key_inrement, etc.
Pros:
You don't have to do anything with calculating numbers.
Cons:
You do have a key split across 2 columns.
It doesn't work with InnoDB.
How about this solution with a trigger and a table to hold the company_ref's uniquely. Made a correction - the reference table has to be MyISAM if you want the numbering to begin at 1 for each unique 4char sequence.
DROP TABLE IF EXISTS company;
CREATE TABLE company (
company_name varchar(100) DEFAULT NULL,
company_ref char(8) DEFAULT NULL
) ENGINE=InnoDB
DELIMITER ;;
CREATE TRIGGER company_reference BEFORE INSERT ON company
FOR EACH ROW BEGIN
INSERT INTO reference SET company_ref=SUBSTRING(LOWER(NEW.company_name), 1, 4), numeric_ref=NULL;
SET NEW.company_ref=CONCAT(SUBSTRING(LOWER(NEW.company_name), 1, 4), LPAD(CAST(LAST_INSERT_ID() AS CHAR(10)), 4, '0'));
END ;;
DELIMITER ;
DROP TABLE IF EXISTS reference;
CREATE TABLE reference (
company_ref char(4) NOT NULL DEFAULT '',
numeric_ref int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (company_ref, numeric_ref)
) ENGINE=MyISAM;
And for completeness here is a trigger that will create a new reference if the company name is altered.
DROP TRIGGER IF EXISTS company_reference_up;
DELIMITER ;;
CREATE TRIGGER company_reference_up BEFORE UPDATE ON company
FOR EACH ROW BEGIN
IF NEW.company_name <> OLD.company_name THEN
DELETE FROM reference WHERE company_ref=SUBSTRING(LOWER(OLD.company_ref), 1, 4) AND numeric_ref=SUBSTRING(OLD.company_ref, 5, 4);
INSERT INTO reference SET company_ref=SUBSTRING(LOWER(NEW.company_name), 1, 4), numeric_ref=NULL;
SET NEW.company_ref=CONCAT(SUBSTRING(LOWER(NEW.company_name), 1, 4), LPAD(CAST(LAST_INSERT_ID() AS CHAR(10)), 4, '0'));
END IF;
END;
;;
DELIMITER ;
Given you're using InnoDB, why not use an explicit transaction to grab an exclusive row lock and prevent another connection from reading the same row before you're done setting a new ID based on it?
(Naturally, doing the calculation in a trigger would hold the lock for less time.)
mysqli_query($link, "BEGIN TRANSACTION");
$query = "SELECT `company_ref`
FROM `companies`
WHERE
`company_ref` LIKE '$fourLetters%'
ORDER BY `company_ref` DESC
LIMIT 1
FOR UPDATE";
$last = mysqli_fetch_assoc(mysqli_query($link, $query));
$newNum = ((int) ltrim(substr($last['company_ref'],4),'0')) + 1;
$newRef = $fourLetters.str_pad($newNum, 4, '0', STR_PAD_LEFT);
mysqli_query($link, "INSERT INTO companies . . . (new row using $newref)");
mysqli_commit($link);
Edit: Just to be 100% sure I ran a test by hand to confirm that the second transaction will return the newly inserted row after waiting rather than the original locked row.
Edit2: Also tested the case where there is no initial row returned (Where you would think there is no initial row to put a lock on) and that works as well.
Ensure you have an unique constraint on the Reference column.
Fetch the current max sequential reference the same way you do it in your sample code. You don't actually need to trim the zeroes before you cast to (int), '0001' is a valid integer.
Roll a loop and do your insert inside.
Check affected rows after the insert. You can also check the SQL state for a duplicate key error, but having zero affected rows is a good indication that your insert failed due to inserting an existing Reference value.
If you have zero affected rows, increment the sequential number, and roll the loop again. If you have non-zero affected rows, you're done and have an unique identifier inserted.
Easiest way to avoid duplicate values for the reference column is to add a unique constraint. So if multiple processes try to set to the same value, MySQL will reject the second attempt and throw an error.
ALTER TABLE table_name ADD UNIQUE KEY (`company_ref`);
If I were faced with your situation, I would handle the company reference id generation within the application layer, triggers can get messy if not setup correctly.
A hacky version that works for InnoDB as well.
Replace the insert to companies with two inserts in a transaction:
INSERT INTO __keys
VALUES (LEFT(LOWER('Smiths Joinery'),4), LAST_INSERT_ID(1))
ON DUPLICATE KEY UPDATE
num = LAST_INSERT_ID(num+1);
INSERT INTO __companies (comp_name, reference)
VALUES ('Smiths Joinery',
CONCAT(LEFT(LOWER(comp_name),4), LPAD(LAST_INSERT_ID(), 4, '0')));
where:
CREATE TABLE `__keys` (
`prefix` char(4) NOT NULL,
`num` smallint(5) unsigned NOT NULL,
PRIMARY KEY (`prefix`)
) ENGINE=InnoDB COLLATE latin1_general_ci;
CREATE TABLE `__companies` (
`comp_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`comp_name` varchar(45) NOT NULL,
`reference` char(8) NOT NULL,
PRIMARY KEY (`comp_id`)
) ENGINE=InnoDB COLLATE latin1_general_ci;
Notice:
latin1_general_ci can be replaced with utf8_general_ci,
LEFT(LOWER('Smiths Joinery'),4) would better become a function in PHP

Unique IDs in MySQL?

So I am working on a fairly small script for the company I work at the help us manage our servers better. I don't use MySQL too often though so I am a bit confused on what would be the best path to take.
I am doing something like this...
$sql = "CREATE TABLE IF NOT EXISTS Servers
(
MachineID int NOT NULL AUTO_INCREMENT,
PRIMARY KEY(MachineID),
FirstName varchar(32),
LastName varchar(32),
Datacenter TINYINT(1),
OperatingSystem TINYINT(1),
HostType TINYINT(1)
)";
$result = mysql_query($sql,$conn);
check ($result);
$sql = "CREATE TABLE IF NOT EXISTS Datacenter
(
DatacenterID int NOT NULL AUTO_INCREMENT,
PRIMARY KEY(DatacenterID),
Name varchar(32),
Location varchar(32)
)";
$result = mysql_query($sql,$conn);
check ($result);
Basically inside the Servers table, I will be storing the index of an entry in another table. My concern is that if we remove one of those entries, it will screw up the auto incremented indexes and potentially cause a lot of incorrect data.
To explain better, lets say the first server I add, the Datacenter value is 3 (which is the DatacenterID), and we remove the id 1 (DatacenterID) Datacenter at a later time.
Is there a good way to do this?
Auto increment only has an effect on inserting new rows into a table. So you insert three records into database and they get assigned ids of 1, 2, and 3. Later you delete id 1, but the records at id 2 and 3 are unchanged. This says nothing of any of the server records that might be trying to reference the database id 1 record though that is now missing.
as said by paul, it is safe to remove old row and add another new one later. The auto incremental index won't be affected by deletion.
But I would suggest instead of removing them, simply add a column 'status' and set 0, implying they are no longer in use, to keep any possible record in db.
Servers.Datacenter should be an INT, too, as you would store the DataCenterID in this field. Then, nothing will be mixed up when you remove some Datacenter from the second table.
If you remove in the way you suggest nothing will happen. auto-increment will just add the next highest number in sequence whenever a new record is added and so will not affect any previous records.
Hope that helps.

Categories