unexpected behaviour of a mysql transaction in a specific concurrency scenario - mysql

I have trouble understanding the behaviour of a mysql transaction in a specific concurrency scenario.
It involves a table with a primary key and another column with a unique key constraint.
In the Java test case give below two DB connections are used concurrently:
The first connection uses a transaction with isolation level "repeatable read", and tries to read a row which is not existing at this point, then tries to insert it using a INSERT ... ON DUPLICATE KEY statement, and tries to read it once more.
The second connection inserts a row with the same value for the unique column AFTER the first read on connection 1 and BEFORE the insert attempt on connection 2.
The output is as follows and I can reproduce it with MySQL 5.7 and 8.0
...
[conTransaction] SELECT * FROM test.tmp WHERE uniq = 'x'; => false
[conAutoCommit ] INSERT INTO test.tmp(uniq, t) VALUES('x', NOW()) ON DUPLICATE KEY UPDATE t = VALUES(t); => 1
[conTransaction] SELECT * FROM test.tmp WHERE uniq = 'x'; => false
[conTransaction] INSERT INTO test.tmp(uniq, t) VALUES('x', NOW()) ON DUPLICATE KEY UPDATE t = VALUES(t); => 1
[conTransaction] SELECT * FROM test.tmp WHERE uniq = 'x'; => false
[conTransaction] COMMIT => 0
[conTransaction] UNEXPECTED: no row found after INSERT ON DUPLICATE KEY
I consider this behaviour unexpected:
the insert in the transaction appears to have succeeded but the row still cannot be read from that transaction.
Is this really correct behaviour for a repeatable read transaction?
More over, if the timing is changed by uncommenting the sleep in the line marked by (A)
then the INSERT ... ON DUPLICATE KEY actually changes something and the row becomes visible:
...
[conTransaction] SELECT * FROM test.tmp WHERE uniq = 'x'; => false
[conAutoCommit ] INSERT INTO test.tmp(uniq, t) VALUES('x', NOW()) ON DUPLICATE KEY UPDATE t = VALUES(t); => 1
[conTransaction] SELECT * FROM test.tmp WHERE uniq = 'x'; => false
[conTransaction] INSERT INTO test.tmp(uniq, t) VALUES('x', NOW()) ON DUPLICATE KEY UPDATE t = VALUES(t); => 2
[conTransaction] SELECT * FROM test.tmp WHERE uniq = 'x'; => true
[conTransaction] COMMIT => 0
This is the sample test case in Java.
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import org.junit.Test;
public class MyTests {
Connection conTA;
Connection conAutoCommit;
#Test
public void test() throws Exception {
conAutoCommit = getConnection();
conTA = getConnection();
conTA.setAutoCommit(false);
setup(conAutoCommit);
boolean hasIt = hasRow(conTA);
assertEquals(false, hasIt);
insert(conAutoCommit);
// Thread.sleep(1200); // (A)
hasRow(conTA);
insert(conTA);
boolean found = hasRow(conTA);
execNonQuery(conTA, "COMMIT");
if (!found) {
log(conTA, "UNEXPECTED: no row found after INSERT ON DUPLICATE KEY");
fail("UNEXPECTED: no row found after INSERT ON DUPLICATE KEY");
}
}
private int execNonQuery(Connection con, String sql) throws SQLException {
try(var stmt = con.createStatement()) {
int res = stmt.executeUpdate(sql);
log(con, sql + " => " + res);
return res;
}
}
private void setup(Connection con) throws SQLException {
execNonQuery(con, "DROP TABLE IF EXISTS test.tmp");
execNonQuery(con, """
CREATE TABLE test.tmp (
id int(11) NOT NULL AUTO_INCREMENT,
uniq varchar(45) DEFAULT NULL,
t datetime DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY serverDomain (uniq)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
""");
}
private void insert(Connection con) throws SQLException {
String sql = "INSERT INTO test.tmp(uniq, t) VALUES('x', NOW()) ON DUPLICATE KEY UPDATE t = VALUES(t);";
execNonQuery(con, sql);
}
private boolean hasRow(Connection con) throws SQLException {
try(var stmt = con.createStatement()) {
String sql = "SELECT * FROM test.tmp WHERE uniq = 'x';";
try(var rs = stmt.executeQuery(sql)) {
boolean res = rs.next();
log(con, sql + " => " + res);
return res;
}
}
}
private void log(Connection con, String msg) {
String conName = con == conTA ? "conTransaction" : "conAutoCommit ";
System.out.println("[" + conName + "] " + msg);
}
private Connection getConnection() throws SQLException {
var con = DriverManager.getConnection("jdbc:mysql://localhost:3306?useSSL=false","root","");
con.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
return con;
}
}

Related

Rollback transaction not working properly

In database manipulation command such as insert, update or delete can sometime throws exception due to invalid data. To protect the integrity of application data we must make sure when we a transaction was failed we must rollback
PreparedStatement ps = null;
Connection conn = null;
try {
conn = DriverManager.getConnection( URL, USERNAME, PASSWORD );
String query = "INSERT INTO tbl1(id, username) " +
"VALUES (?, ?)";
ps = conn.prepareStatement( query );
ps.setString( 1, "javaduke" );
ps.execute();
query = "INSERT INTO tbl2 (id, tbl1_id, " +
"quantity, price) VALUES (?, ?, ?, ?)";
ps = conn.prepareStatement( query );
ps.setInt( 1, id );
ps.setInt( 2, tbl_id );
ps.setInt( 3, 10 );
ps.setDouble( 4, 29.99 );
ps.execute();
}
catch ( SQLException e )
{
conn.rollback()
e.printStackTrace();
}
I guess this is Java.
Right after you get your connection object, turn off autocommit, like so.
conn = DriverManager.getConnection( URL, USERNAME, PASSWORD );
conn.setAutoCommit(false);
Right after your last execute() do this.
conn.commit();
Then the rollback() in your exception handler should do what you expect.
This should extend the scope of your transaction to beyond a single SQL query.

Inserting Timestamp into MySQL from Java application

I am creating a Java application that communicates with MySQL database. Using XAMPP 5.6.33-0 and phpMyAdmin. I have the following method that, among other values, inserts a Timestamp into the table RATING:
PreparedStatement pst = myConn.prepareStatement("INSERT INTO RATING
(ratingDate) VALUES(?)");
java.util.Date today = new java.util.Date();
Timestamp ts = new java.sql.Timestamp(today.getTime());
pst.setTimestamp(1, ts);
pst.executeUpdate();
The schema of the RATING relation looks as follows:
CREATE TABLE RATING
(cID INT,
rID INT,
stars INT,
ratingDate TIMESTAMP,
FOREIGN KEY(cID) REFERENCES CUSTOMER(cID) on delete cascade,
FOREIGN KEY(rID) REFERENCES ROOM(rID)
) ;
So attribute ratingDate is defined as Timestamp. Everything works great except when the Timestamp is inserted its value is always set to all zeros: 0000-00-00 00:00:00
I tried converting the Timestamp to string using t.toString and can clearly see that the Timestamp object is created properly. It seems the problem is with setTimestamp() method. Also, converting the data type of ratingDate to just Date and using setDate() method works fine, but setTimestamp() function always sets the attribute value to all zeros.
There are, of course, workaround for this. I could declare the date as varchar, convert Timestamp to a String and insert it using setString() but I am really wondering what the problem may be. Running Eclipse with Tomcat server. No errors in console.
Thank you in advance for any help, I'd be happy to provide any other necessary information.
Avoid legacy date-time classes
The all-zeros values is a mystery. But I can tell you that you are using terrible date-time classes that were supplanted years ago by the java.time classes with the adoption of JSR 310. This is making your work more complicated than it needs to be.
I suggest creating a simple dummy table to narrow down your problem.
OffsetDateTime odt = OffsetDateTime.now( ZoneOffset.UTC ) ;
myPreparedStatement.setObject( … , odt ) ;
Retrieval.
OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime ) ;
Example app
I do not use MySQL. But here is a complete example app using the H2 Database Engine.
package work.basil.example;
import org.h2.jdbcx.JdbcDataSource;
import java.sql.*;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class H2DateTimeExample
{
public static void main ( String[] args )
{
H2DateTimeExample app = new H2DateTimeExample ();
app.demo ();
}
private void demo ( )
{
JdbcDataSource dataSource = new JdbcDataSource ();
dataSource.setURL ( "jdbc:h2:mem:offsetdatetime_example_db;DB_CLOSE_DELAY=-1" ); // Set `DB_CLOSE_DELAY` to `-1` to keep in-memory database in existence after connection closes.
dataSource.setUser ( "scott" );
dataSource.setPassword ( "tiger" );
// Create table.
String sql = "CREATE TABLE person_ ( \n" +
" pkey_ UUID NOT NULL DEFAULT RANDOM_UUID() PRIMARY KEY , \n" +
" name_ VARCHAR NOT NULL , \n" +
"first_contacted_ TIMESTAMP WITH TIME ZONE NOT NULL " +
") ;";
// System.out.println ( sql );
try (
Connection conn = dataSource.getConnection () ;
Statement stmt = conn.createStatement () ;
)
{
stmt.execute ( sql );
} catch ( SQLException e )
{
e.printStackTrace ();
}
// Insert row.
sql = "INSERT INTO person_ ( name_ , first_contacted_ ) \n";
sql += "VALUES ( ? , ? ) \n";
sql += ";";
try (
Connection conn = dataSource.getConnection () ;
PreparedStatement pstmt = conn.prepareStatement ( sql , Statement.RETURN_GENERATED_KEYS ) ;
)
{
OffsetDateTime odt = OffsetDateTime.now ( ZoneOffset.UTC );
pstmt.setString ( 1 , "Jesse Johnson" );
pstmt.setObject ( 2 , odt );
pstmt.executeUpdate ();
ResultSet rs = pstmt.getGeneratedKeys ();
// System.out.println( "INFO - Reporting generated keys." );
// while ( rs.next() ) {
// UUID uuid = rs.getObject( 1 , UUID.class );
// System.out.println( "generated keys: " + uuid );
// }
} catch ( SQLException e )
{
e.printStackTrace ();
}
// Query table.
sql = "TABLE person_ ;";
try (
Connection conn = dataSource.getConnection () ;
PreparedStatement pstmt = conn.prepareStatement ( sql ) ;
)
{
try ( ResultSet rs = pstmt.executeQuery () ; )
{
while ( rs.next () )
{
UUID pkey = rs.getObject ( "pkey_" , UUID.class );
String name = rs.getString ( "name_" );
OffsetDateTime firstContacted = rs.getObject ( "first_contacted_" , OffsetDateTime.class );
System.out.println ( "pkey: " + pkey + " | name: " + name + " | firstContacted: " + firstContacted );
}
}
} catch ( SQLException e )
{
e.printStackTrace ();
}
System.out.println ( "Done." );
}
}
When run.
pkey: b14fd25f-1598-4f09-9475-83ac5967a338 | name: Jesse Johnson | firstContacted: 2019-07-28T02:10:07.731005Z
Done.
After some additional research I figured it out. The problem was that the java Timestamp object uses milliseconds at the end while the timestamp attribute in the MySQL table didn't (it was in the format "yyyy-MM-dd HH:mm:ss"). So this mismatch prevented the insertion of the correct timestamp and instead put a tuple with all zeros into MySQL table. The solution is to format the Timestamp object in the java code to cut off the milliseconds and then insert the Timestamp object into MySQL table:
java.util.Date today = new java.util.Date();
java.sql.Timestamp timestamp = new java.sql.Timestamp(today.getTime());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
pst.setObject(4, formatter.format(timestamp));
This worked like a charm. Hope it helps somebody!

MySQL JDBC Syntax Error

I am writing a simple database with a query that inserts some data, modifies a entry, deletes it, then prints out the rest.
import java.sql.*;
public class SpotifyDB {
//JDBC driver name and database URL
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
static final String DB_URL = "jdbc:mysql://localhost/spotify";
static final int portNumber = 3306;
static final String serverName = "localhost";
static final String dbName = "spotify";
//Database credentials
static final String USER = "root";
static final String PASS = "root";
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try{
//Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");
//Open a connection to the database
System.out.println("Connecting to a selected database...");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
System.out.println("Connected database successfully...");
//Insert data
System.out.println("Inserting records into the table...");
stmt = conn.createStatement();
String sql = "INSERT INTO artist(artist) " +
"values('Muse')";
stmt.executeUpdate(sql);
sql = "INSERT INTO album(album, artist, genre, year)" +
"values('Drones', 'Muse', 'Rock', 2015)";
stmt.executeUpdate(sql);
sql = "INSERT INTO album(album, artist, genre, year)" +
"values('The 2nd Law', 'Muse', 'Rock', 2012)";
stmt.executeUpdate(sql);
sql = ("INSERT INTO songs(song, artist, album, tracknumber, duration)" +
"values('Madness', 'Muse', 'The 2nd Law', 2, '4:41')");
stmt.executeUpdate(sql);
sql = ("INSERT INTO songs(song, artist, album, tracknumber, duration)" +
"values('Mercy', 'Muse', 'Drones', 4, '3:52')");
stmt.executeUpdate(sql);
System.out.println("Records inserted into the table!");
//Update data
String sql1 = "UPDATE songs " +
"SET track number = 1 WHERE song in ('Madness')";
stmt.executeUpdate(sql1);
//Delete data
String sql2 = "DELETE FROM songs " +
"WHERE song = Madness";
stmt.executeUpdate(sql2);
//View records
String sql3 = "SELECT * FROM songs";
ResultSet rs = stmt.executeQuery(sql3);
while(rs.next()) {
//Retrieve by column name
String song = rs.getString("song");
String artist = rs.getString("artist");
String album = rs.getString("album");
String track = rs.getString("track number");
String duration = rs.getString("duration");
//Display the values
System.out.print("Song: " + song);
System.out.print(", Artist: " + artist);
System.out.print(", Album: " + album);
System.out.println(", Track: " + track);
System.out.println(", Duration: " + duration);
}
//Close the connection, clean up running functions
rs.close();
stmt.close();
conn.close();
}
catch(SQLException se) {
//Handle errors for JDBC driver
se.printStackTrace();
}
catch(Exception e) {
//Handle errors for Class.forName
e.printStackTrace();
}
finally {
//finally used to close resources
try{
if(stmt!=null)
stmt.close();
}
catch(SQLException se2) {
}
try{
if(conn!=null)
conn.close();
}
catch(SQLException se) {
se.printStackTrace();
}
}
System.out.println("Goodbye!");
}
}
My SQL Database table is quite simple as well;
CREATE TABLE spotify.`songs` (
`song` varchar(20) NOT NULL,
`artist` varchar(20) NOT NULL,
`album` varchar(20) NOT NULL,
`track number` int(3) NOT NULL,
`duration` varchar(10) NOT NULL,
PRIMARY KEY (`song`),
KEY `songalbum_idx` (`album`),
KEY `songartist` (`artist`),
CONSTRAINT `songalbum` FOREIGN KEY (`album`) REFERENCES `album` (`album`)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `songartist` FOREIGN KEY (`artist`) REFERENCES `artist` (`artist`)
ON DELETE CASCADE
ON UPDATE CASCADE);
and the console is returning me this error: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException
I am having trouble seeing where the table columns are not matching up, any help would be appreciated. Thank you!
Column names really shouldn't have spaces, for exactly this reason. But if your column names must have spaces then you need to qualify them with back-ticks exactly as you do in your CREATE TABLE statement:
INSERT INTO songs (song, artist, album, `track number`, duration) VALUES ...
Otherwise after the identifier track the query engine is expecting either a comma (to move on to another identifier) or a close parentheses (to end the list of column identifiers). It finds neither of this, and immediately finds another identifier (number, which may even be a reserved word?). This confuses the query parser.

Insert values in existing record. sqlite using prepared statement

I want to insert values into an existing record using prepared statment. The value of that record is the last record inserted, I tried LAST function and MAX but it didn't seems to work, also I tried SELECT statment inside INSERT with no luck.
public static void insertCoordinates(){
java.sql.Connection c = null;
try {
String str1 = Singelton.getInstance().getTxtField1().getText();
Class.forName("org.sqlite.JDBC");
c = DriverManager.getConnection("jdbc:sqlite:timeline_DB.db");
c.setAutoCommit(false);
PreparedStatement preparedStatement = c.prepareStatement("insert into event_table(bar_length, x_bar, y_bar) values (?, ?, ?) WHERE event_title =?");
preparedStatement.setInt(1, CanvasNewTimeLine.Event_length);
preparedStatement.setInt(2, CanvasNewTimeLine.x);
preparedStatement.setInt(3, CanvasNewTimeLine.H_LINE);
preparedStatement.setString(4,str1);
preparedStatement.executeUpdate();
c.commit();
c.close();
} catch ( Exception e ) {
System.err.println( e.getClass().getName() + ": " + e.getMessage() );
System.exit(0);
System.err.println("problem");
}
System.out.println("yesssssss successfully");
}
I used update instead of insert:
public static void insertCoordinates(){
java.sql.Connection c = null;
try {
String str1 = Singelton.getInstance().getTxtField1().getText();
Class.forName("org.sqlite.JDBC");
c = DriverManager.getConnection("jdbc:sqlite:timeline_DB.db");
c.setAutoCommit(false);
String updateTableSQL = "UPDATE event_table SET bar_length = ?, x_bar=?, y_bar=? WHERE event_title = ?";
PreparedStatement preparedStatement = c.prepareStatement(updateTableSQL);
preparedStatement.setInt(1, CanvasNewTimeLine.Event_length);
preparedStatement.setInt(2, CanvasNewTimeLine.x);
preparedStatement.setInt(3, CanvasNewTimeLine.H_LINE);
preparedStatement.setString(4,str1);
// execute
preparedStatement .executeUpdate();
c.commit();
c.close();
} catch ( Exception e ) {
System.err.println( e.getClass().getName() + ": " + e.getMessage() );
System.exit(0);
System.err.println("problem");
}
System.out.println("yesssssss successfully");
}

Linq to sql error inserting value

I am trying to do an insert using linq to sql but am getting the following error
Additional information: Cannot insert the value NULL into column 'UserID', table 'Itiss_Request.dbo.Users'; column does not allow nulls. INSERT fails.
The UserID table is the pk aswel as the identity has been set to autoincrement.
The database has 4 fields.
DataClasses1DataContext dt = new DataClasses1DataContext();
User usr = new User();
usr.MudID = a[1];
usr.Email = Session["email"].ToString();
usr.Name = Session["userName"].ToString();
dt.Users.InsertOnSubmit(usr);
dt.SubmitChanges();
This is an from my context file
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_UserID", AutoSync=AutoSync.OnInsert, DbType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)]
public int UserID
{
get
{
return this._UserID;
}
set
{
if ((this._UserID != value))
{
this.OnUserIDChanging(value);
this.SendPropertyChanging();
this._UserID = value;
this.SendPropertyChanged("UserID");
this.OnUserIDChanged();
}
}
}
Please try this...
DataClasses1DataContext dt = new DataClasses1DataContext();
User usr = new User();
usr.MudID = a[1];
usr.Email = Session["email"].ToString();
usr.Name = Session["userName"].ToString();
dt.Users.AddObject(usr);
dt.SaveChanges();