Java and RDBMS Married with issues constraints Speaker

Jeroen van Schagen Situation

store Java Relational Application Database retrieve JDBC JDBC

• Java Database Connectivity

Access API ( java., 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 Check

Unique key Length

Type 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 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 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 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 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 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

org.jarbframework jarb-constraints 2.1.0

Github

http://www.jarbframework.org Questions?