Java and RDBMS Married with issues Database constraints Speaker
Jeroen van Schagen Situation
store Java Relational Application Database retrieve JDBC JDBC
• Java Database Connectivity
• Data Access API ( java.sql, javax.sql )
• JDK 1.1 (1997)
• Many implementations 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); Database
Maintain data User
name : varchar(3) NOT-NULL, UNIQUE Name can only have up to 3 characters
Name is required
Name can only occur once Constraint types
Not null Check
Unique key Length
Type Foreign key Primary key User name : varchar(3) NOT-NULL, UNIQUE
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql);
What happens?
Assuming the user table is empty User name : varchar(3) NOT-NULL, UNIQUE
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql);
1 row updated User name : varchar(3) NOT-NULL, UNIQUE
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql); statement.executeUpdate(sql);
WhatWhat will happens? happen? User name : varchar(3) NOT-NULL, UNIQUE
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql); statement.executeUpdate(sql); SQLIntegrityConstraint WhatViolationException will happen? executeUpdate(sql) INSERT
return 1 Inserted 1
Applicatio JDBC Database n executeUpdate(sql) INSERT
throw Unique violation SQLIntegrityConstraint ViolationException User name : varchar(3) NOT-NULL, UNIQUE
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (‘Jan’)”; statement.executeUpdate(sql); statement.executeUpdate(sql); User name : 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”); } User name : varchar(3) NOT-NULL, UNIQUE
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (NULL)”; statement.executeUpdate(sql);
What happens? User name : varchar(3) NOT-NULL, UNIQUE
Connection connection = …; Statement statement = connection.createStatement();
String sql = “INSERT INTO user (name) VALUES (NULL)”; statement.executeUpdate(sql);
SQLIntegrityConstraint ViolationException User name : 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) { throw new RuntimeException(“Name is required”); throw new RuntimeException(“Name already exists”); } Unique key violation
SQLIntegrityConstraint ViolationException
Not null violation Unique key violation
SQLIntegrityConstraint ViolationException
Not null violation 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 User name : 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”); } } User name : 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”); } } Complicated Boilerplate Assumptions Multiple not-null values
User
name : varchar(3) NOT-NULL, UNIQUE email : varchar(30) NOT-NULL, UNIQUE Multiple not-null values Multiple unique values
Which was violated? User
name : varchar(3) NOT-NULL, uk_user_name UNIQUE email : varchar(30) NOT-NULL, uk_user_email UNIQUE SQLException
+ getSQLState() : int + getMessage() : String
SQLIntegrityConstraint ViolationException Vendor messages
They are all diferent Oracle ORA-00001: unique constraint (GOTO.UK_USER_NAME) violated\n MySQL Duplicate entry 'Jan' for key 'uk_user_name' 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.
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] Vendor messages
The info is there Oracle ORA-00001: unique constraint (GOTO.UK_USER_NAME) violated\n MySQL Duplicate entry 'Jan' for key 'uk_user_name' 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.
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] Extract violation info
• Message Just too difcult
• Pattern matching
Focus on application logic • Vendor specific Concrete exception classes
UniqueKeyViolationException
NotNullViolationException
JDBC needs a better exception API ( for integrity constraints )
Access to constraint info
getColumnName()
getConstraintName() Workaround Prevent violations Prevent violations
• Data integrity checks in application layer. Prevent not-null
if (user.getName() == null) { throw new RuntimeException(“Name is required”); } Javax validation
public class User { @NotNull private String name; } No SQL exception
Conveys
Less database interaction Less interaction
throw new RuntimeException
Applicatio Database n Duplication
Application Database
User User
@NotNull name : varchar(3) private String name NOT-NULL, UNIQUE Duplication
Application Database
User User
@NotNull name : varchar(3) private String name NOT-NULL, UNIQUE
Kept in sync Unexpected SQL exceptions Prevent unique violation
• Complicated
• Depends on other rows id name
NULL
Testable in isolation id name
Jan id name
Jan
Requires data users id name
1 Piet 2 Jan
3 Henk No SQL exceptions
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); }
Extra query Not atomic Problem: Not atomic
Thread 1 COUNT WHERE name = ‘Jan’ return 0
Thread 2 INSERT (name) VALUES (‘Jan’) Applicatio Database n INSERTED 1 Uncaught Thread 1 Unexpected INSERT (name) VALUES (‘Jan’)
Unique key violation Decision on old data Recap Lack proper solution
Not null
No SQL exceptions Duplication Error prone
Unique key
No SQL exceptions Extra query Error prone Solution Java Repository Bridge - JaRB Databases are good at maintaining integrity;
let them! Prevent exception Catch exception Testable in isolation
Not null Unique key
Type Foreign key
Length Primary key
Check Prevent exception
Validation
Not null Type Length User name : varchar(3) NOT-NULL, UNIQUE @Entity email : varchar(100) @DatabaseConstrained public class User { @NotNull @Length(max=3) private String name; private String email; Retrieve } constraints
Database as No duplication only truth validate(new User(‘Henk’));
1. Loop over properties 3. Check name ‘Henk’ on metadata name = ‘Henk’ email = null Application
2. Get metadata varchar(3) user.name not null Determine column name (Hibernate) Database “ Name cannot be longer than 3 characters “
validate(new User(‘Henk’));
1. Loop over properties 3. Check name ‘Henk’ on metadata name = ‘Henk’ email = null Application
2. Get metadata varchar(3) user.name not null
Database validate(new User(‘Henk’)); validate(new User(null));
1. Loop over properties 3. Check null name on metadata name = null email = null Application
2. Get metadata varchar(3) user.name not null
Database “ Name cannot be null “
validate(new User(‘Henk’)); validate(new User(null));
1. Loop over properties 3. Check null name on metadata name = null email = null Application
2. Get metadata varchar(3) user.name not null
Database validate(new User(‘Henk’)); validate(new User(null)); validate(new User(‘Jan’));
1. Loop over properties 3. Check name ‘Jan’ on metadata name = ‘Jan’ email = null Application
2. Get metadata varchar(3) user.name not null
Database validate(new User(‘Henk’)); validate(new User(null)); validate(new User(‘Jan’));
1. Loop over properties 3. Check name ‘Jan’ on metadata name = ‘Jan’ email = null Application
2. Get metadata varchar(3) user.name not null
Database validate(new User(‘Henk’)); validate(new User(null)); validate(new User(‘Jan’));
1. Loop over properties 3. Check null email on metadata name = ‘Jan’ email = null Application
2. Get metadata varchar(100) user.email
Database validate(new User(‘Henk’)); validate(new User(null)); validate(new User(‘Jan’));
1. Loop over properties 3. Check null email on metadata name = ‘Jan’ email = null Application
2. Get metadata varchar(100) user.email
Database validate(new User(‘Henk’)); validate(new User(null)); validate(new User(‘Jan’));
1. Loop over properties 3. Check null email on metadata name = ‘Jan’ email = null Application
2. Get metadata varchar(100) user.email
Database Super class
@MappedSuperclass @DatabaseConstrained public abstract class BaseEntity { }
@Entity public class User extends BaseEntity { private String name; private String email; } JDBC Custom schema mapper
@DatabaseConstrained public class User { private String name; private String email; } Catch exception
Exception translation
Unique key Foreign key Primary key Check Translate the JDBC exception into a proper constraint exception Existing translators Hibernate
• Object Relation Mapping
• Extracts constraint name from message Hibernate
Access to constraint name
ConstraintViolationException getConstraintName()
Heavy for plain JDBC Hardcoded names Hardcoded names
try { // Insert user } catch (ConstraintViolationException e) { if (e.getConstraintName() == “uk_user_name”) { // Handle error } }
Too technical
Focus on domain Spring
• Dependency Injection
• Templates
• JDBC
• DAO Spring JDBC
• JdbcTemplate
• SQLExceptionTranslator
• Error codes
• Register own classes
• No constraint name Spring
Consistent hierarchy Extensible
DataAccessException
DataIntegrityViolationException Spring DAO
• ORM (e.g. Hibernate)
• PersistenceExceptionTranslator
• Proxy UserRepository
Spring$Proxy
ConstraintViolation JPASystemException Exception
PersistenceExceptionTranslator Hierarchy
DataAccessException
cause ConstraintViolationException JPASystemException getConstraintName()
No constraint name
Weaker API Weaker API
Unsafe cast try { userRepository.save(user); } catch (JPASystemException e) { ConstraintViolationException ce = (ConstraintViolationException) e.getCause(); if (ce.getConstraintName() == “uk_user_name”) { // Handle error } }
Why isn’t this easier? Recap Best of both worlds
Hibernate Spring JaRB
Constraint name
Hierarchy
Extensible 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 UserNameAlready ConstraintException ExistsException Resolver
Extract all information from exception
SQLIntegrity ConstraintException
ERROR: duplicate key value violates unique constraint \"uk_user_name\" Detail: Key (name)=(Jan) already exists. Resolver
Extract all information from exception
SQLIntegrity ConstraintException Constraint name ERROR: duplicate key value violates unique constraint \"uk_user_name\" Detail: Key (name)=(Jan) already exists. Pattern matching
Vendor specific Column name Value Version specific Resolvers
• Pattern matching (default) • PostgreSQL • Oracle • MySQL • HSQL • H2
• Hibernate: constraint name only Factory
Create a concrete exception Default factory
InvalidTypeException
LengthExceededViolationExceptio n
CheckFailedException
NotNullViolationException
PrimaryKeyViolationException
ForeignKeyViolationException
UniqueKeyViolationException Constraint info DatabaseConstraintViolationException
InvalidTypeException
LengthExceededViolationExceptio n
CheckFailedException
NotNullViolationException
PrimaryKeyViolationException
ForeignKeyViolationException
UniqueKeyViolationException
UserNameAlreadyExistsException 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(…) { }
} Throwable (cause) DatabaseConstraintViolation 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 Resolve database vendor Register custom exceptions
Enable database validation Get source
Maven central
Github
http://www.jarbframework.org Questions?