George_Sherston has asked for the wisdom of the Perl Monks concerning the following question:

Sistren and Brethren. Let me tell you my tale.

My app gets input from far and wide via CGI; it then does two things with this input: (A) enters it into a MySQL table, generating an AUTO_INCREMENT LineID for each new record; (B) sends an email to the user, which, inter alia, refers to the newly-created record by giving the new LineID. (For this example, col headings in tbl are LineID MEDIUMINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, Col2, Col3.) So my first thought was, I do:
$dbh->do("INSERT INTO tbl VALUES(NULL,'foo','bar')");
and then either
my $ref = $dbh->selectcol_arrayref( "SELECT MAX(LineID) FROM tbl" ); my $LineID = $$ref_booking[0];
or
my $ref = $dbh->selectcol_arrayref( "SELECT LAST_INSERT_ID() FROM tbl ");
But I tested this by making my script sleep between the INSERT and the SELECT, and whilst it was asleep running a second INSERT from another script. And in both cases, the result was that the SELECT returns the LineID for the record created by the second INSERT.

So now I think I need to do some table locking. And think I know how to do this in SQL. But I'd rather keep it in the family, and use the eval {..} construct mentioned in the docs:
$dbh->{AutoCommit} = 0; # enable transactions, if possible $dbh->{RaiseError} = 1; eval { $dbh->do("INSERT INTO tbl VALUES(NULL,'foo','bar')"); my $ref = $dbh->selectcol_arrayref( "SELECT MAX(LineID) FROM tbl" ); $dbh->commit; # commit the changes if we get this far }; if ($@) { warn "Transaction aborted because $@"; $dbh->rollback; # undo the incomplete changes # add other application on-error-clean-up code here }
... but, and here I tear out what little hair I have left, my MySQL doesn't support transactions.

So my question is, is there a perl way to get the same result as locking? Or should I just make the effort and learn some of the basics about MySQL?

(With thanks to all those who helped me get this far in the CB this morning - and I hasten to add, I've been working on other stuff too since then!)

§ George Sherston

Replies are listed 'Best First'.
Re: DBI: table locking
by thpfft (Chaplain) on Sep 17, 2001 at 20:58 UTC

    You can get the last insert id from mySQL on a per-handle basis by asking for:

    my $new_id = $dbh->{'mysql_insertid'};

    Which should get around the problem, unless you're using Apache::DBI or something else that pools database handles.

    update: remembered something i saw in the lower reaches of Class::DBI::Extension: you can lock a mysql table using:

    $dbh->do('LOCK TABLES mumble WRITE');

    and later

    $dbh->do('UNLOCK TABLES');

    But it's a rather drastic thing to do, and one of the main reasons some people are rude about MySQL.

      Hrrmm... alas, when I try this on the same basis as the methods I did above, I still end up returning the last ID inserted, rather than the ID inserted by the querying script. I fear there may be no alternative to table locking.

      § George Sherston
Re: DBI: table locking
by dga (Hermit) on Sep 17, 2001 at 23:45 UTC

    I see a bit of PostgreSQL discussion here

    You can get what you want with a sequence in PgSQL.

    #earlier in SQL CREATE TABLE tbl ( id serial, col2 text, col3 text ); NOTICE: CREATE TABLE will create implicit sequence 'tbl_id_seq' for S +ERIAL column 'tbl.id'
    my $myid; my $dbh=DBI->connect("DBI:Pg(AutoCommit=>0,RaiseError=>1):dbname=foo") +; eval { $dbh->do("INSERT INTO tbl ( col2, col3 ) VALUES ( 'val', 'val3' )" +; my $sth=$dbh->prepare("SELECT currval('tbl_id_seq')"); $sth->execute; $sth->bind_columns( \($myid )); $sth->fetch; $dbh->commit; }; if($@) { $dbh->rollback; #enlightening error message goes here } # $myid has your id in it here.

    You would put a sequence type as a column in the table with autoincrement running etc then it would have a name and you select currval('your_sequence_name_here') which PostgreSQL guarantees will be the one from the most recently inserted row in the current transaction.

    Also I like to avoid wildcards in my SQL statements from inside scrips so that its obvious the intent of the script without having to look in the database to find out what columns etc are being affected.

    Actually even without transactions, I am pretty sure currval() will return the last id that your process used on the given sequence.

Re: DBI: table locking
by pmas (Hermit) on Sep 17, 2001 at 21:02 UTC
    I believe MySQL does not support transactions. I heard PostgreSQL does have transactions, and it was discussed it in PM, I believe. Super Search? I am struggling with Oracle... HTH

    pmas
    To make errors is human. But to make million errors per second, you need a computer.

      MySQL does indeed support transactions but it requires you use the right table type (preferably BDB or InnoDB). So you may have to do some re-compilation/installation before you can get transactions working, but they're definitely there.
      HTH

      broquaint

      Glad to hear it's not just me. The version of MySQL I have, which is quite recent, certainly doesn't support transactions.

      § George Sherston
Re: DBI: table locking
by George_Sherston (Vicar) on Sep 18, 2001 at 01:41 UTC
    For the sake of completeness, in case any other monk happens upon this thread in future years, I shall mention here the solution that I've settled on, after reviewing above and some comments in the CB:
    $dbh->do("LOCK TABLES tbl WRITE"); $dbh->do("INSERT INTO tbl VALUES(NULL,'foo','bar')"); my $ref = $dbh->selectcol_arrayref(" SELECT LAST_INSERT_ID() FROM tbl"); $dbh->do("UNLOCK TABLES");
    This is not the best solution. The best solution would be either PostGreSQL, or getting transactions to work in MySQL. But given where I'm up to with the project I'm working on I'm going to have to live with the second-bestness of it. A bit of a sledgehammer to crack a nut... but it DOES crack the nut!

    Thanks again to all those who've helped me with this.

    update: crazyinsomniac /msg'd the following even better alternative:

    from DBD::mysql
    DATABASE HANDLES The DBD::mysql driver supports the following attributes of database ha +ndles (read only): $infoString = $dbh->{'info'}; $threadId = $dbh->{'thread_id'}; $insertId = $dbh->{'mysql_insertid'} These correspond to mysql_info(), mysql_thread_id() and mysql_insertid +(), respectively. insertid MySQL has the ability to choose unique key values automatically. If th +is happened, the new ID will be stored in this attribute. This attrib +u +te is not valid for DBD::mSQL. An alternative way for accessing this +a +ttribute is via $dbh->{'mysql_insertid'}. (Note we are using the $dbh + +in this case!)
    So, each instance of your script will have its own database handle, and will only do one INSERT, so the 'mysql_insertid' will be the right one. There is no chance of the insertid being that of a subsequent INSERT that a different process performed (you're not sharing the handle, mysql will not fuck up in this manner).

    § George Sherston