Java and RDBMS Married with issues Database constraints
Speaker Jeroen van Schagen
Situation Java Application store retrieve JDBC Relational Database
JDBC Java Database Connectivity Data Access API ( java.sql, javax.sql ) JDK 1.1 (1997) Relational Database 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 can only have up to 3 characters name : varchar(3) NOT-NULL, UNIQUE Name is required Name can only occur once
Constraint types Not null Check Length Unique key Primary key Type Foreign 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); What 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 What will happen? ViolationException
executeupdate(sql) INSERT return 1 Inserted 1 Applicatio n executeupdate(sql) JDBC INSERT Database 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 User Which was violated? name : varchar(3) NOT-NULL, UNIQUE email : varchar(30) NOT-NULL, UNIQUE uk_user_name uk_user_email
SQLException + getsqlstate() : int + getmessage() : String SQLIntegrityConstraint ViolationException
Vendor messages Oracle ORA-00001: unique constraint (GOTO.UK_USER_NAME) violated\n They are all different 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 Oracle ORA-00001: unique constraint (GOTO.UK_USER_NAME) violated\n The info is there 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 difficult Pattern matching Vendor specific Focus on application logic
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 n Database
Duplication Application User Database User @NotNull private String name name : varchar(3) NOT-NULL, UNIQUE
Duplication Application User Database User @NotNull private String name name : varchar(3) 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 1 2 3 name Piet Jan 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 Applicatio n Thread 2 Thread 1 INSERT (name) VALUES ( Jan ) INSERTED 1 INSERT (name) VALUES ( Jan ) Unique key violation Database Uncaught Unexpected 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 Testable in isolation Catch exception Not null Type Length Unique key Foreign key Primary key Check
Prevent exception Validation Not null Type Length
@Entity @DatabaseConstrained public class User { @NotNull @Length(max=3) private String name; private String email; } User name : varchar(3) NOT-NULL, UNIQUE email : varchar(100) Retrieve constraints Database as only truth No duplication
validate(new User( Henk )); 1. Loop over properties 3. Check name Henk on metadata name = Henk email = null Application 2. Get metadata user.name varchar(3) 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 user.name varchar(3) not null Database
validate(new User( Henk )); validate(new User(null)); 1. Loop over properties name = null email = null 3. Check null name on metadata Application 2. Get metadata user.name varchar(3) not null Database
Name cannot be null validate(new User( Henk )); validate(new User(null)); 1. Loop over properties name = null email = null 3. Check null name on metadata Application 2. Get metadata user.name varchar(3) 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 user.name varchar(3) 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 user.name varchar(3) not null Database
validate(new User( Henk )); validate(new User(null)); validate(new User( Jan )); 1. Loop over properties name = Jan email = null 3. Check null email on metadata Application 2. Get metadata user.email varchar(100) Database
validate(new User( Henk )); validate(new User(null)); validate(new User( Jan )); 1. Loop over properties name = Jan email = null 3. Check null email on metadata Application 2. Get metadata user.email varchar(100) Database
validate(new User( Henk )); validate(new User(null)); validate(new User( Jan )); 1. Loop over properties name = Jan email = null 3. Check null email on metadata Application 2. Get metadata user.email varchar(100) 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 Exception JPASystemException PersistenceExceptionTranslator
Hierarchy DataAccessException JPASystemException cause ConstraintViolationException 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 ConstraintException UserNameAlready 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 ERROR: duplicate key value violates unique constraint \"uk_user_name\" Detail: Key (name)=(jan) already exists. Constraint name Pattern matching Column name Value Vendor specific 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
DatabaseConstraintViolationException Constraint info 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 ) <jarb:enable-constraints base-package= org.myproject /> Enable exception translation Resolve database vendor Register custom exceptions Enable database validation
Get source Maven central <dependency> <groupid>org.jarbframework</groupid> <artifactid>jarb-constraints</artifactid> <version>2.1.0</version> </dependency> Github http://www.jarbframework.org
Questions?