java and rdbmsjaoo.dk/dl/goto-amsterdam-2014/slides/jeroenvanschagen... · 2014. 6. 20. ·...
TRANSCRIPT
Java and RDBMSMarried with issues
Database constraints
Speaker
Jeroen van Schagen
Situation
store
retrieveJava
ApplicationRelational Database
JDBC
• Java Database Connectivity
• Data Access API ( java.sql, javax.sql )
• JDK 1.1 (1997)
• Relational Database
• Many implementations
JDBC
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql);
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql);
User
name : varchar(3) NOT-NULL, UNIQUE
Name can only haveup to 3 characters
Name is required
Name can only occur once
Database
Maintain data
Constraint types
Not null
Type
Length
Primary key Foreign key
Unique key
Check
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql);
Username : varchar(3) NOT-NULL, UNIQUE
What happens?
Assuming the user table is empty
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql);
Username : varchar(3) NOT-NULL, UNIQUE
1 row updated
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql); statement.executeUpdate(sql);
What will happen?
Username : varchar(3) NOT-NULL, UNIQUE
What happens?
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql); statement.executeUpdate(sql);
What will happen?
Username : varchar(3) NOT-NULL, UNIQUE
SQLIntegrityConstraintViolationException
Application JDBC Database
executeUpdate(sql) INSERT
return 1 Inserted 1
executeUpdate(sql) INSERT
Unique violationthrowSQLIntegrityConstraint
ViolationException
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql); statement.executeUpdate(sql);
Username : varchar(3) NOT-NULL, UNIQUE
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; try { statement.executeUpdate(sql); statement.executeUpdate(sql); } catch (SQLIntegrityConstraintViolationException e) { throw new RuntimeException(“Name already exists”);}
Username : varchar(3) NOT-NULL, UNIQUE
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (NULL)”; statement.executeUpdate(sql);
Username : varchar(3) NOT-NULL, UNIQUE
What happens?
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (NULL)”; statement.executeUpdate(sql);
Username : varchar(3) NOT-NULL, UNIQUE
SQLIntegrityConstraintViolationException
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (NULL)”; try { statement.executeUpdate(sql); } catch (SQLIntegrityConstraintViolationException e) { throw new RuntimeException(“Name is required”); throw new RuntimeException(“Name already exists”);}
Username : varchar(3) NOT-NULL, UNIQUE
Unique key violation
SQLIntegrityConstraint ViolationException
Not null violation
Unique key violation
Not null violation
SQLIntegrityConstraint ViolationException
Which was violated?
SQLException
+ getSQLState() : int+ getMessage() : String
SQLIntegrityConstraint ViolationException
SQLException
+ getSQLState() : int+ getMessage() : String
SQLIntegrityConstraint ViolationException
State Name
23000 Integrity constraint
23001 Restrict violation
23502 Not null violation
23503 Foreign key violation
23505 Unique violation
23514 Check violation
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (NULL)”; try { statement.executeUpdate(sql); } catch (SQLIntegrityConstraintViolationException e) { if (e.getSQLState() == 23502) { throw new RuntimeException(“Name is required”); } else if (e.getSQLState() == 23505) { throw new RuntimeException(“Name already exists”); } }
Username : varchar(3) NOT-NULL, UNIQUE
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (NULL)”; try { statement.executeUpdate(sql); } catch (SQLIntegrityConstraintViolationException e) { if (e.getSQLState() == 23502) { throw new RuntimeException(“Name is required”); } else if (e.getSQLState() == 23505) { throw new RuntimeException(“Name already exists”); } }
Username : varchar(3) NOT-NULL, UNIQUE
Complicated Boilerplate Assumptions
Multiple not-null values
User
name : varchar(3) NOT-NULL, UNIQUEemail : varchar(30) NOT-NULL, UNIQUE
uk_user_name
uk_user_email
Multiple not-null valuesMultiple unique values
User
name : varchar(3) NOT-NULL, UNIQUEemail : varchar(30) NOT-NULL, UNIQUE
Which was violated?
SQLException
+ getSQLState() : int+ getMessage() : String
SQLIntegrityConstraint ViolationException
Vendor messages
HSQL integrity constraint violation: unique constraint or index violation;
UK_USER_NAME table: USER
PostgreSQL ERROR: duplicate key value violates unique constraint \"uk_user_name\"
Detail: Key (name)=(Jan) already exists.
Oracle ORA-00001: unique constraint (GOTO.UK_USER_NAME) violated\n
MySQL Duplicate entry 'Jan' for key 'uk_user_name'
H2 Unique index or primary key violation: "UK_USER_NAME_INDEX_1 ON GOTO.USER(NAME)";
SQL statement:\ninsert into user (name) values (?) [23505-171]
They are all different
Vendor messages
HSQL integrity constraint violation: unique constraint or index violation;
UK_USER_NAME table: USER
PostgreSQL ERROR: duplicate key value violates unique constraint \"uk_user_name\"
Detail: Key (name)=(Jan) already exists.
Oracle ORA-00001: unique constraint (GOTO.UK_USER_NAME) violated\n
MySQL Duplicate entry 'Jan' for key 'uk_user_name'
H2 Unique index or primary key violation: "UK_USER_NAME_INDEX_1 ON GOTO.USER(NAME)";
SQL statement:\ninsert into user (name) values (?) [23505-171]
The info is there
• Message
• Pattern matching
• Vendor specific
Extract violation info
Just too difficult
Focus on application logic
JDBC needs a better exception API ( for integrity constraints )
Concrete exception classes
UniqueKeyViolationException
NotNullViolationException
Access to constraint info
getColumnName()
getConstraintName()
Workaround
Prevent violations
• Data integrity checks in application layer.
Prevent violations
Prevent not-null
if (user.getName() == null) { throw new RuntimeException(“Name is required”); }
Javax validation
public class User { @NotNull private String name; }
Conveys
No SQL exception
Less database interaction
Application
Database
throw new RuntimeException
Less interaction
User@NotNull private String name
User
name : varchar(3) NOT-NULL, UNIQUE
Application Database
Duplication
User@NotNull private String name
User
name : varchar(3) NOT-NULL, UNIQUE
Application Database
Kept in sync Unexpected SQL exceptions
Duplication
• Complicated
• Depends on other rows
Prevent unique violation
id name
NULL
Testable in isolation
id name
Jan
id name
id nameusers
Piet
Henk
Jan
1
2
3
Jan
Requires data
No SQL exceptions
Extra query Not atomic
if (countUsersWithName(user.getName()) > 0) { throw new RuntimeException(“Name already exists”);}
private int countUsersWithName(String name) { return jdbcTemplate.queryForObject( “SELECT COUNT(1) FROM user where name = ?”, name, Long.class); }
Problem: Not atomic
Application Database
INSERT (name) VALUES (‘Jan’)
INSERTED 1
Thread 2
COUNT WHERE name = ‘Jan’return 0
Thread 1
INSERT (name) VALUES (‘Jan’)
Unique key violation
Thread 1
Decision onold data
UnexpectedUncaught
Recap
No SQL exceptions Error proneDuplication
Not null
Extra query
Unique key
No SQL exceptions Error prone
Lack proper solution
SolutionJava Repository Bridge - JaRB
Databases are good atmaintaining integrity;
let them!
Catch exceptionPrevent exceptionTestable in isolation
Not null
Type
Length
Check
Unique key
Foreign key
Primary key
Prevent exceptionValidation
Not null Type Length
@Entity @DatabaseConstrained public class User { @NotNull @Length(max=3) private String name; private String email; }
Username : varchar(3) NOT-NULL, UNIQUE email : varchar(100)
No duplication
Retrieve constraints
Database as only truth
validate(new User(‘Henk’));
varchar(3) not null
name = ‘Henk’ email = null
1. Loop over properties 3. Check name ‘Henk’ on metadata
Database
Application
2. Get metadata user.name
Determine column name (Hibernate)
Database
Applicationname = ‘Henk’ email = null
varchar(3) not null
1. Loop over properties 3. Check name ‘Henk’ on metadata
“ Name cannot be longer than 3 characters “
2. Get metadata user.name
validate(new User(‘Henk’));
validate(new User(‘Henk’));
name = null email = null
1. Loop over properties 3. Check null name on metadata
validate(new User(null));
varchar(3) not null
Database
Application
2. Get metadata user.name
validate(new User(‘Henk’));
Applicationname = null email = null
varchar(3) not null
1. Loop over properties 3. Check null name on metadata
validate(new User(null));
“ Name cannot be null “
Database
2. Get metadata user.name
validate(new User(‘Henk’));
name = ‘Jan’ email = null
1. Loop over properties 3. Check name ‘Jan’ on metadata
validate(new User(null));validate(new User(‘Jan’));
Database
Application
varchar(3) not null
2. Get metadata user.name
validate(new User(‘Henk’));
Database
Applicationname = ‘Jan’ email = null
varchar(3) not null
1. Loop over properties 3. Check name ‘Jan’ on metadata
2. Get metadata user.name
validate(new User(null));validate(new User(‘Jan’));
validate(new User(‘Henk’));
name = ‘Jan’ email = null
varchar(100)
1. Loop over properties 3. Check null email on metadata
Database
Application
2. Get metadata user.email
validate(new User(null));validate(new User(‘Jan’));
validate(new User(‘Henk’));
Database
Applicationname = ‘Jan’ email = null
varchar(100)
1. Loop over properties 3. Check null email on metadata
2. Get metadata user.email
validate(new User(null));validate(new User(‘Jan’));
validate(new User(‘Henk’));
Database
Applicationname = ‘Jan’ email = null
varchar(100)
1. Loop over properties 3. Check null email on metadata
2. Get metadata user.email
validate(new User(null));validate(new User(‘Jan’));
Super class@MappedSuperclass @DatabaseConstrained public abstract class BaseEntity { }
@Entity public class User extends BaseEntity { private String name; private String email; }
JDBC
@DatabaseConstrained public class User { private String name; private String email; }
Custom schema mapper
Catch exceptionException translation
CheckUnique key Foreign key Primary key
Translate the JDBC exception into a proper constraint exception
Existing translators
• Object Relation Mapping
• Extracts constraint name from message
Hibernate
Hibernate
ConstraintViolationExceptiongetConstraintName()
Access to constraint name
Hardcoded namesHeavy for plain JDBC
Hardcoded names
try { // Insert user } catch (ConstraintViolationException e) { if (e.getConstraintName() == “uk_user_name”) { // Handle error } }
Too technical
Focus on domain
• Dependency Injection
• Templates
• JDBC
• DAO
Spring
• JdbcTemplate
• SQLExceptionTranslator
• Error codes
• Register own classes
• No constraint name
Spring JDBC
Spring
DataAccessException
DataIntegrityViolationException
Consistent hierarchy
Extensible
• ORM (e.g. Hibernate)
• PersistenceExceptionTranslator
• Proxy
Spring DAO
Spring$Proxy
PersistenceExceptionTranslator
ConstraintViolation Exception JPASystemException
UserRepository
No constraint name
ConstraintViolationExceptiongetConstraintName()
JPASystemException
DataAccessException
cause
Hierarchy
Weaker API
Weaker API
try { userRepository.save(user); } catch (JPASystemException e) { ConstraintViolationException ce = (ConstraintViolationException) e.getCause(); if (ce.getConstraintName() == “uk_user_name”) { // Handle error } }
Unsafe cast
Why isn’t this easier?
Recap
Constraint name
Hierarchy
Extensible
Hibernate Spring JaRB
Best of both worlds
JaRB
Concrete and domain specific exceptions.
Map each constraint to a custom exception.
try { userRepository.save(new User(“Jan”)); } catch (UserNameAlreadyExistsException e) { error(“User name already exists.”); }
try { userRepository.save(new User(“Jan”)); } catch (UserNameAlreadyExistsException e) { error(“User name already exists.”); } catch (UserEmailAlreadyExistsException e) { error(“User email already exists.”); }
Translator
SQLIntegrity ConstraintException
UserNameAlready ExistsException
Resolver
ERROR: duplicate key value violates unique constraint \"uk_user_name\" Detail: Key (name)=(Jan) already exists.
Extract all information from exception
SQLIntegrity ConstraintException
Resolver
ERROR: duplicate key value violates unique constraint \"uk_user_name\" Detail: Key (name)=(Jan) already exists.
Vendor specific
Pattern matching
Extract all information from exception
Column name Value
Constraint name
Version specific
SQLIntegrity ConstraintException
Resolvers
• Pattern matching (default) • PostgreSQL • Oracle • MySQL • HSQL • H2
• Hibernate: constraint name only
Factory
Create a concrete exception
Default factory
UniqueKeyViolationException
NotNullViolationException
LengthExceededViolationException
PrimaryKeyViolationException
CheckFailedException
InvalidTypeException
ForeignKeyViolationException
DatabaseConstraintViolationException
UniqueKeyViolationException
NotNullViolationException
LengthExceededViolationException
PrimaryKeyViolationException
CheckFailedException
InvalidTypeException
ForeignKeyViolationException
UserNameAlreadyExistsException
Constraint info
Custom exceptions
@NamedConstraint(“uk_user_name”) public class UserNameAlreadyExistsException extends UniqueKeyViolationException { }
Scanned from class path
Registered on constraint
Custom exceptions
uk_user_name UserNameAlreadyExistsException
uk_user_email UniqueKeyViolationException
Injectable arguments
@NamedConstraint(“uk_user_name”) public class UserNameAlreadyExistsException extends UniqueKeyViolationException { UserNameAlreadyExistsException(…) { } }
DatabaseConstraintViolationThrowable (cause)
ExceptionFactory
Less concrete
try { userRepository.save(new User(“Jan”)); } catch (UniqueKeyViolationException e) { error(“User name already exists.”); }
How?
Enable in Spring@EnableDatabaseConstraints(basePackage = “org.myproject”)
Enable exception translation
Enable database validation
Resolve database vendorRegister custom exceptions
<jarb:enable-constraints base-package=“org.myproject”/>
Get source
<dependency> <groupId>org.jarbframework</groupId> <artifactId>jarb-constraints</artifactId> <version>2.1.0</version></dependency>
Maven central
http://www.jarbframework.org
Github
Questions?