Slide 1

Slide 1 text

Embracing SQL
 Without Abstraction Alec Strong

Slide 2

Slide 2 text

Cursor returned zero rows SELECT knowledge
 FROM alec
 WHERE venue='Droidcon'
 LIMIT 40min;

Slide 3

Slide 3 text

What we want: • RxJava • Immutability • Type Safety • Compiler Errors • SQLite features

Slide 4

Slide 4 text

What we’re given: • Android’s SQLite bindings • SQLiteOpenHelper • Cursor • SQLiteDatabase • ContentValues • ContentProvider/ContentResolver

Slide 5

Slide 5 text

What we’re given: db.rawQuery("SELECT * FROM user", new String[0]);

Slide 6

Slide 6 text

What we’re given: db.rawQuery("SELECT * FROM user WHERE name IN (?, ?, ?)",
 new String[] { "John", "Nelson", "Logan" });

Slide 7

Slide 7 text

What we’re given: db.rawQuery("SELECT * FROM " + User.TABLE_NAME + " WHERE name IN (?, ?, ?)",
 new String[] { "John", "Nelson", "Logan" });

Slide 8

Slide 8 text

What we’re given: db.rawQuery("SELECT * FROM " + User.TABLE_NAME + 
 " WHERE " + User.NAME + " IN (?, ?, ?)",
 new String[] { "John", "Nelson", "Logan" });

Slide 9

Slide 9 text

What we want: RxJava Immutability Type Safety Compiler Errors SQLite features

Slide 10

Slide 10 text

What did we do: • Research

Slide 11

Slide 11 text

Research • Reflection • Synchronous • Lack of Type Safety • Underpowered Query APIs • Missing SQLite features

Slide 12

Slide 12 text

Scaffold @Table(User.TABLE_NAME)
 public interface User {
 String TABLE_NAME = "user";
 
 String USER_ID = "user_id";
 String NAME = "name";
 String PHOTO_URL = "photo_url";
 
 @PrimaryKey @Column(USER_ID) @AutoIncrement @NotNull
 String lookupKey();
 
 @NotNull @Column(NAME)
 String name();
 
 @NotNull @Column(PHOTO_URL)
 String photoUrl();
 }

Slide 13

Slide 13 text

Scaffold @Table(User.TABLE_NAME)
 public interface User {
 } public static final class ScUser implements User {
 public static Func1> mapRow();
 public static Func1 single();
 }

Slide 14

Slide 14 text

Scaffold @Database (
 tables = {
 User.class
 },
 fileName = "my_database.db",
 version = 1,
 authority = BuildConfig.PACKAGE_NAME + ".data.db.Provider"
 )
 public final class MyDatabase extends ScaffoldDatabase {
 public MyDatabase(Context context) {
 super(context.getContentResolver());
 }
 }

Slide 15

Slide 15 text

Scaffold Observable user = myDatabase.createQuery(
 Query.from(User.class).where(EQ(User.NAME, "John")))
 .map(ScUser.single());

Slide 16

Slide 16 text

Scaffold Observable payments = myDatabase.createQuery(Query
 .from(Payment.class)
 .join(Customer.class)
 .on(EQ(Customer.TOKEN, Payment.CUSTOMER_TOKEN)))
 .join(User.class)
 .on(EQ(Customer.USER_ID, User.USER_ID))
 .where(EQ(Payment.TOKEN, paymentToken));

Slide 17

Slide 17 text

Observable payments = myDatabase.createQuery(Query
 .from(Payment.class)
 .join(Customer.class)
 .on(EQ(Customer.TOKEN, Payment.CUSTOMER_TOKEN)))
 .join(User.class)
 .on(EQ(Customer.USER_ID, User.USER_ID))
 .where(EQ(Payment.TOKEN, paymentToken)); SELECT *
 FROM payment
 JOIN customer USING (customer_token)
 JOIN user USING (user_id)
 WHERE token=?;

Slide 18

Slide 18 text

Observable payments = myDatabase.createQuery(Query
 .from(Payment.class)
 .join(Customer.class)
 .on(EQ(Customer.TOKEN, Payment.CUSTOMER_TOKEN)))
 .join(User.class, JoinType.LEFT)
 .on(EQ(Customer.USER_ID, User.USER_ID))
 .where(EQ(Payment.TOKEN, paymentToken)); SELECT *
 FROM payment
 JOIN customer USING (customer_token)
 LEFT JOIN user USING (user_id)
 WHERE token=?;

Slide 19

Slide 19 text

Observable payments = myDatabase.createQuery(Query
 .from(Payment.class)
 .join(Customer.class)
 .on(EQ(Customer.TOKEN, Payment.CUSTOMER_TOKEN)))
 .join(User.class, JoinType.LEFT)
 .on(EQ(Customer.USER_ID, User.USER_ID))
 .where(EQ(Payment.TOKEN, paymentToken));
 .where(AND(
 EQ(Payment.TOKEN, paymentToken),
 OR(EQ(User.USER_ID, null), EQ(User.NAME, “Logan”))
 )); SELECT *
 FROM payment
 JOIN customer USING (customer_token)
 LEFT JOIN user USING (user_id)
 WHERE token=?;
 AND (user.used_id IS NULL OR user.name = 'Logan');

Slide 20

Slide 20 text

Scaffold • Many flaws • QueryBuilder • Closed API • Too busy as a library • RxJava, ContentProvider, QueryBuilder, SQLiteOpenHelper, Cursor -> Object mapping, Object -> ContentValues Marshaling • Prone to bugs • Inner queries and joins

Slide 21

Slide 21 text

What we want: RxJava Immutability Type Safety Compiler Errors SQLite features

Slide 22

Slide 22 text

Not Scaffold • Need to address type safety • IntColumn, StringColumn • How do you model scope

Slide 23

Slide 23 text

Not Scaffold • Need to address type safety • IntColumn, StringColumn • How do you model scope SELECT *
 FROM customer
 WHERE customer_name = (
 SELECT customer_name -- Works fine, variable is scoped in.
 FROM user
 ORDER BY customer_name -- No such column found.
 );

Slide 24

Slide 24 text

Putting Away The Scaffolding

Slide 25

Slide 25 text

What we want: RxJava Immutability Type Safety Compiler Errors SQLite features

Slide 26

Slide 26 text

What we want: RxJava

Slide 27

Slide 27 text

SQLBrite • Won’t do perfect emission • False positives • But how do we know when a table is updated? • Require table names

Slide 28

Slide 28 text

@AutoValue public abstract class User {
 public static final String TABLE_NAME = "user";
 
 public static final String USER_ID = "user_id";
 public static final String NAME = "name";
 public static final String PHOTO_URL = "photo_url";
 
 public static final String CREATE_TABLE = format("CREATE TABLE %s (" //
 + "%s INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL" //
 + ")", TABLE_NAME, USER_ID, NAME, PHOTO_URL);

Slide 29

Slide 29 text


 public abstract long userId();
 public abstract String name();
 public abstract String photoUrl();


Slide 30

Slide 30 text


 public static final Func1 MAPPER = cursor -> {
 return new AutoValue_User(
 cursor.getLong(cursor.getColumnIndexOrThrow(USER_ID)),
 cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
 cursor.getString(cursor.getColumnIndexOrThrow(PHOTO_URL))
 );
 };

Slide 31

Slide 31 text


 public static final class Builder {
 private final ContentValues contentValues = new ContentValues();
 
 public Builder userId(long userId) {
 contentValues.put(USER_ID, userId);
 return this;
 }A
 
 public Builder name(String name) {
 contentValues.put(NAME, name);
 return this;
 }A
 
 public Builder photoUrl(String photoUrl) {
 contentValues.put(PHOTO_URL, photoUrl);
 return this;
 }A
 
 public ContentValues build() {
 return contentValues;
 }A
 }A
 }A

Slide 32

Slide 32 text

@AutoValue public abstract class User {
 public static final String TABLE_NAME = "user";
 
 public static final String USER_ID = "user_id";
 public static final String NAME = "name";
 public static final String PHOTO_URL = "photo_url";
 
 public static final String CREATE_TABLE = format("CREATE TABLE %s (" //
 + "%s INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL" //
 + ")", TABLE_NAME, USER_ID, NAME, PHOTO_URL);

Slide 33

Slide 33 text

@AutoValue public abstract class User {
 public static final String TABLE_NAME = "user";
 
 public static final String USER_ID = "user_id";
 public static final String NAME = "name";
 public static final String PHOTO_URL = "photo_url";
 public static final String BIRTH_DATE = “birth_date";
 
 
 public static final String CREATE_TABLE = format("CREATE TABLE %s (" //
 + "%s INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL,” //
 + "%s TEXT NOT NULL" //
 + ")", TABLE_NAME, USER_ID, NAME, PHOTO_URL, BIRTH_DATE);

Slide 34

Slide 34 text


 public abstract long userId();
 public abstract String name();
 public abstract String photoUrl();


Slide 35

Slide 35 text


 public abstract long userId();
 public abstract String name();
 public abstract String photoUrl();
 public abstract Calendar birthDate();


Slide 36

Slide 36 text


 public static final Func1 MAPPER = cursor -> {
 return new AutoValue_User(
 cursor.getLong(cursor.getColumnIndexOrThrow(USER_ID)),
 cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
 cursor.getString(cursor.getColumnIndexOrThrow(PHOTO_URL))
 );
 };

Slide 37

Slide 37 text


 public static final Func1 MAPPER = cursor -> {
 Calendar calendar = Calendar.getInstance();
 calendar.setTimeInMillis( cursor.getLong(cursor.getColumnIndexOrThrow(BIRTH_DATE)));
 return new AutoValue_User(
 cursor.getLong(cursor.getColumnIndexOrThrow(USER_ID)),
 cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
 cursor.getString(cursor.getColumnIndexOrThrow(PHOTO_URL)),
 calendar
 );
 };

Slide 38

Slide 38 text


 public static final class Builder {
 private final ContentValues contentValues = new ContentValues();
 
 public Builder userId(long userId) {
 contentValues.put(USER_ID, userId);
 return this;
 }A
 
 public Builder name(String name) {
 contentValues.put(NAME, name);
 return this;
 }A
 
 public Builder photoUrl(String photoUrl) {
 contentValues.put(PHOTO_URL, photoUrl);
 return this;
 }A
 
 public ContentValues build() {
 return contentValues;
 }A
 }A
 }A

Slide 39

Slide 39 text


 public Builder userId(long userId) {
 contentValues.put(USER_ID, userId);
 return this;
 }A
 
 public Builder name(String name) {
 contentValues.put(NAME, name);
 return this;
 }A
 
 public Builder photoUrl(String photoUrl) {
 contentValues.put(PHOTO_URL, photoUrl);
 return this;
 }A
 
 public Builder birthDate(Calendar birthDate) {
 contentValues.put(BIRTH_DATE, birthDate.getTimeInMillis());
 return this;
 }
 
 public ContentValues build() {
 return contentValues;


Slide 40

Slide 40 text

SQLBrite Observable> users = db.createQuery(User.TABLE_NAME,
 "SELECT * FROM " + User.TABLE_NAME + 
 " WHERE " + User.NAME + " IN (?, ?, ?)",
 new String[] { "John", "Nelson", "Logan" })
 .map(query -> {
 Cursor cursor = query.run();
 List users = new ArrayList<>();
 try {
 users.add(User.MAPPER.call(cursor));
 } finally {
 cursor.close();
 } return users;
 });

Slide 41

Slide 41 text

SQLBrite Observable> users = db.createQuery(User.TABLE_NAME,
 "SELECT * FROM " + User.TABLE_NAME +
 " WHERE " + User.NAME + " IN (?, ?, ?)",
 new String[] { "John", "Nelson", "Logan" })
 .mapToList(User.MAPPER);

Slide 42

Slide 42 text

SQLBrite • Solved Rx problem • Requires table name and has false positives

Slide 43

Slide 43 text

MarshalMappers • Aimed at limiting boilerplate • AutoValue extension • Short life span

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

@AutoValue public abstract class User {
 public static final String TABLE_NAME = "user";
 
 public static final String USER_ID = "user_id";
 public static final String NAME = "name";
 public static final String PHOTO_URL = "photo_url";
 public static final String BIRTH_DATE = "birth_date";
 
 public static final String CREATE_TABLE = format("CREATE TABLE %s (" //
 + "%s INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL" //
 + ")", TABLE_NAME, USER_ID, NAME, PHOTO_URL, BIRTH_DATE);


Slide 46

Slide 46 text


 public abstract long userId();
 public abstract String name();
 public abstract String photoUrl();
 public abstract Calendar birthDate();

Slide 47

Slide 47 text


 @Column(USER_ID) public abstract long userId();
 @Column(NAME) public abstract String name();
 @Column(PHOTO_URL) public abstract String photoUrl();
 @Column(value = BIRTH_DATE, marshalMapper = CalendarMarshalMapper.class)
 public abstract Calendar birthDate();

Slide 48

Slide 48 text


 
 public static final Func1 MAPPER = cursor -> {
 Calendar calendar = Calendar.getInstance();
 calendar.setTimeInMillis( cursor.getLong(cursor.getColumnIndexOrThrow(BIRTH_DATE)));
 return new AutoValue_User(
 cursor.getLong(cursor.getColumnIndexOrThrow(USER_ID)),
 cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
 cursor.getString(cursor.getColumnIndexOrThrow(PHOTO_URL)),
 calendar
 );
 };


Slide 49

Slide 49 text


 
 public static final Func1 MAPPER =
 cursor -> User_Mapper.map(cursor, AutoValue_User::new); 


Slide 50

Slide 50 text


 public static final class Builder {
 private final ContentValues contentValues = new ContentValues();
 
 public Builder userId(long userId) {
 contentValues.put(USER_ID, userId);
 return this;
 }
 
 public Builder name(String name) {
 contentValues.put(NAME, name);
 return this;
 }
 
 public Builder photoUrl(String photoUrl) {
 contentValues.put(PHOTO_URL, photoUrl);
 return this;
 }
 
 public Builder birthDate(Calendar birthDate) {
 contentValues.put(BIRTH_DATE, birthDate.getTimeInMillis());
 return this;
 }
 


Slide 51

Slide 51 text


 public static final class Marshal extends User_Marshal { }

Slide 52

Slide 52 text

@AutoValue public abstract class User {
 public static final String TABLE_NAME = "user";
 
 public static final String USER_ID = "user_id";
 public static final String NAME = "name";
 public static final String PHOTO_URL = "photo_url";
 public static final String BIRTH_DATE = "birth_date";
 
 public static final String CREATE_TABLE = format("CREATE TABLE %s (" //
 + "%s INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL" //
 + ")", TABLE_NAME, USER_ID, NAME, PHOTO_URL, BIRTH_DATE);
 
 @Column(USER_ID) public abstract long userId();
 @Column(NAME) public abstract String name();
 @Column(PHOTO_URL) public abstract String photoUrl();
 @Column(value = BIRTH_DATE, marshalMapper = CalendarMarshalMapper.class)
 public abstract Calendar birthDate(); 
 public static final Func1 MAPPER =
 cursor -> User_Mapper.map(cursor, AutoValue_User::new); 
 public static final class Marshal extends User_Marshal { }

Slide 53

Slide 53 text

MarshalMappers • Uses reflection for custom types • Still has the AutoValue “fourth wall” • No real direction

Slide 54

Slide 54 text

The Turning Point • IntelliJ plugin vs Annotation Processor • Schema cannot be in java • Why not just use SQL? • Only additional information needed for java creation is type

Slide 55

Slide 55 text

@AutoValue public abstract class User {
 public static final String TABLE_NAME = "user";
 
 public static final String USER_ID = "user_id";
 public static final String NAME = "name";
 public static final String PHOTO_URL = "photo_url";
 public static final String BIRTH_DATE = “birth_date"; 
 
 public static final String CREATE_TABLE = String.format("CREATE TABLE %s (" //
 + "%s INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL" //
 + ")", TABLE_NAME, USER_ID, NAME, PHOTO_URL, BIRTH_DATE);
 
 @Column(USER_ID) public abstract long userId();
 @Column(NAME) public abstract String name();
 @Column(PHOTO_URL) public abstract String photoUrl();
 CREATE TABLE user (
 user_id INTEGER PRIMARY KEY AUTOINCREMENT,
 name TEXT NOT NULL,
 photo_url TEXT NOT NULL,
 birth_date TEXT NOT NULL
 );

Slide 56

Slide 56 text

@AutoValue public abstract class User {
 public static final String CREATE_TABLE = format("CREATE TABLE %s (" //
 + "%s INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL," //
 + "%s TEXT NOT NULL" //
 + ")", TABLE_NAME, USER_ID, NAME, PHOTO_URL, BIRTH_DATE);
 
 @Column(USER_ID) public abstract long userId();
 @Column(NAME) public abstract String name();
 @Column(PHOTO_URL) public abstract String photoUrl();
 @Column(value = BIRTH_DATE, marshalMapper = CalendarMarshalMapper.class)
 public abstract Calendar birthDate();
 
 public static final Func1 MAPPER =
 cursor -> User_Mapper.map(cursor, AutoValue_User::new);
 
 public static final class Marshal extends User_Marshal { }
 } CREATE TABLE user (
 user_id INTEGER PRIMARY KEY AUTOINCREMENT,
 name TEXT NOT NULL,
 photo_url TEXT NOT NULL,
 birth_date TEXT NOT NULL
 );

Slide 57

Slide 57 text

@AutoValue public abstract class User {
 public abstract long userId();
 public abstract String name();
 public abstract String photoUrl();
 public abstract Calendar birthDate(); 
 public static final Func1 MAPPER =
 cursor -> User_Mapper.map(cursor, AutoValue_User::new);
 
 public static final class Marshal extends User_Marshal { }
 } CREATE TABLE user (
 user_id INTEGER PRIMARY KEY AUTOINCREMENT,
 name TEXT NOT NULL,
 photo_url TEXT NOT NULL,
 birth_date TEXT NOT NULL
 );

Slide 58

Slide 58 text

@AutoValue public abstract class User {
 public abstract long userId();
 public abstract String name();
 public abstract String photoUrl();
 public abstract Calendar birthDate(); 
 public static final Func1 MAPPER =
 cursor -> User_Mapper.map(cursor, AutoValue_User::new);
 
 public static final class Marshal extends User_Marshal { }
 } CREATE TABLE user (
 user_id INTEGER PRIMARY KEY AUTOINCREMENT,
 name TEXT NOT NULL,
 photo_url TEXT NOT NULL,
 birth_date TEXT AS java.util.Calendar NOT NULL
 );

Slide 59

Slide 59 text

@AutoValue public abstract class User implements UserModel {
 public static final UserMapper MAPPER = new UserMapper( AutoValue_User::new, CALENDAR_ADAPTER);
 
 public static final class Marshal extends UserMarshal { Marshal() {
 super(CALENDAR_ADAPTER);
 } }
 } CREATE TABLE user (
 user_id INTEGER PRIMARY KEY AUTOINCREMENT,
 name TEXT NOT NULL,
 photo_url TEXT NOT NULL,
 birth_date TEXT AS java.util.Calendar NOT NULL
 );

Slide 60

Slide 60 text

CREATE TABLE user (
 user_id INTEGER PRIMARY KEY AUTOINCREMENT,
 name TEXT NOT NULL,
 photo_url TEXT NOT NULL,
 birth_date TEXT AS java.util.Calendar NOT NULL
 ); select_squares:
 SELECT *
 FROM user
 WHERE name IN ('John', 'Nelson', 'Logan');

Slide 61

Slide 61 text

What we want: Immutability Type Safety Compiler Errors SQLite features

Slide 62

Slide 62 text

What we want: Immutability Type Safety Compiler Errors SQLite features

Slide 63

Slide 63 text

Type Safety select_squares:
 SELECT *
 FROM user
 WHERE name IN ('John', 'Nelson', 'Logan'); • Already know table schemas • NoColumnDefinition name

Slide 64

Slide 64 text

A Proper IDE select_squares:
 SELECT *
 FROM user
 WHERE | IN ('John', 'Nelson', 'Logan'); • IntelliJ features |

Slide 65

Slide 65 text

A Proper IDE select_squares:
 SELECT *
 FROM user
 WHERE | IN ('John', 'Nelson', 'Logan'); • IntelliJ features • Enables fast query development |

Slide 66

Slide 66 text

Projections select_squares:
 SELECT *
 FROM user
 WHERE name IN ('John', 'Nelson', 'Logan'); • SELECT * SQL Anti-pattern SELECT * select_squares:
 SELECT name, photo_url
 FROM user
 WHERE name IN ('John', 'Nelson', 'Logan'); SELECT name, photo_url

Slide 67

Slide 67 text

Projections select_squares:
 SELECT *
 FROM user
 WHERE name IN ('John', 'Nelson', 'Logan'); • SELECT * SQL Anti-pattern • Generate mapper per query SELECT * select_squares:
 SELECT name, photo_url
 FROM user
 WHERE name IN ('John', 'Nelson', 'Logan'); SELECT name, photo_url

Slide 68

Slide 68 text

Projections select_squares:
 SELECT *
 FROM user
 WHERE name IN ('John', 'Nelson', 'Logan'); • SELECT * SQL Anti-pattern • Generate mapper per query • Creates strong bond between view and query SELECT * select_squares:
 SELECT name, photo_url
 FROM user
 WHERE name IN ('John', 'Nelson', 'Logan'); SELECT name, photo_url

Slide 69

Slide 69 text

select_squares:
 SELECT name, photo_url
 FROM user
 WHERE name IN ('John', 'Nelson', 'Logan'); Benefits • Shared Language • Confidence writing SQL • Tight coupling with views • Easier code review

Slide 70

Slide 70 text

select_squares:
 SELECT name, photo_url
 FROM user
 WHERE name IN (?, ?, ?); What’s next? (?, ?, ?) public SqlDelightStatement select_squares( String name1, String name2, String name3);

Slide 71

Slide 71 text

select_squares:
 SELECT name, photo_url
 FROM user
 WHERE name IN ?; What’s next? ? public SqlDelightStatement select_squares( String name1, String name2, String name3); public SqlDelightStatement select_squares(String[] name);

Slide 72

Slide 72 text

What’s next? select_squares:
 SELECT name, photo_url
 FROM user
 WHERE name = :name OR last_name = :name; name = :name OR last_name = :name public SqlDelightStatement select_squares( String name1, String name2, String name3); public SqlDelightStatement select_squares(String[] name); public SqlDelightStatement select_squares(String name);

Slide 73

Slide 73 text

• Migration Verification • Testing support • Kotlin What’s next? select_squares:
 SELECT *
 FROM user
 WHERE name = :name OR last_name = :name;

Slide 74

Slide 74 text

SQLBrite select_squares:
 SELECT *
 FROM user
 WHERE name = :name OR last_name = :name; user

Slide 75

Slide 75 text

• Table names known SQLBrite select_squares:
 SELECT *
 FROM user
 WHERE name = :name OR last_name = :name; user

Slide 76

Slide 76 text

• Table names known • So are filters SQLBrite select_squares:
 SELECT *
 FROM user
 WHERE name = :name OR last_name = :name; user

Slide 77

Slide 77 text

first_name last_name email github twitter melee_tag password Alec Strong [email protected] AlecStrong @Strongolopolis RewinD hunter2 contact_info