├── .gitignore ├── README.md ├── pom.xml ├── src ├── main │ └── java │ │ └── uk │ │ └── co │ │ └── benjiweber │ │ └── benjiql │ │ ├── ddl │ │ ├── Create.java │ │ ├── CreateRelationship.java │ │ └── JoinTables.java │ │ ├── mocking │ │ ├── DefaultValues.java │ │ ├── Recorder.java │ │ └── RecordingObject.java │ │ ├── query │ │ ├── Join.java │ │ ├── JoinCondition.java │ │ ├── JoinSpecifier.java │ │ ├── QueryChain.java │ │ ├── RelationshipJoin.java │ │ ├── RelationshipJoinSpecifier.java │ │ └── Select.java │ │ ├── results │ │ ├── ClassMapper.java │ │ ├── Mapper.java │ │ ├── RecordMapper.java │ │ └── SettableField.java │ │ ├── update │ │ ├── Delete.java │ │ ├── DeleteFromRelationship.java │ │ ├── FieldNameValue.java │ │ ├── InsertRelationship.java │ │ └── Upsert.java │ │ └── util │ │ ├── Conventions.java │ │ ├── Exceptions.java │ │ └── ExecutionException.java └── test │ └── java │ └── uk │ └── co │ └── benjiweber │ └── benjiql │ ├── ddl │ └── CreateTest.java │ ├── example │ ├── Conspiracy.java │ ├── Person.java │ ├── RealExample.java │ └── RealExampleWithRecords.java │ ├── query │ ├── SelectRecordTest.java │ └── SelectTest.java │ └── update │ ├── DeleteTest.java │ ├── InsertTest.java │ └── UpdateTest.java └── todo /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.class 3 | *.iml 4 | .idea/ 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | benjiql 2 | ======= 3 | 4 | Proof of concept for database interaction using new features in Java 8 5 | 6 | It aims to be as typesafe as possible and do no compile-time code generation. 7 | 8 | It's best explained by example. 9 | 10 | ```java 11 | 12 | @Test public void example_of_create_table_persist_retrieve_and_update_with_real_database() throws SQLException { 13 | create(Person.class) 14 | .field(Person::getFirstName) 15 | .field(Person::getLastName) 16 | .field(Person::getFavouriteNumber) 17 | .execute(this::openConnection); 18 | 19 | delete(Person.class) 20 | .where(Person::getLastName) 21 | .equalTo("weber") 22 | .execute(this::openConnection); 23 | 24 | Person benji = new Person("benji","weber"); 25 | benji.setFavouriteNumber(9); 26 | 27 | insert(benji) 28 | .value(Person::getFirstName) 29 | .value(Person::getLastName) 30 | .value(Person::getFavouriteNumber) 31 | .execute(this::openConnection); 32 | 33 | benji.setFirstName("benji-updated"); 34 | 35 | update(benji) 36 | .value(Person::getFirstName) 37 | .where(Person::getLastName) 38 | .equalTo("weber") 39 | .execute(this::openConnection); 40 | 41 | Mapper personMapper = Mapper.mapper(Person::new) 42 | .set(Person::setFirstName) 43 | .set(Person::setLastName) 44 | .set(Person::setFavouriteNumber); 45 | 46 | Optional result = from(Person.class) 47 | .where(Person::getFirstName) 48 | .like("%updated") 49 | .and(Person::getLastName) 50 | .equalTo("weber") 51 | .select(personMapper, this::openConnection); 52 | 53 | assertEquals("benji-updated", result.get().getFirstName()); 54 | assertEquals("weber", result.get().getLastName()); 55 | assertEquals((Integer)9, result.get().getFavouriteNumber()); 56 | } 57 | 58 | @Test public void example_of_select_with_join() throws SQLException { 59 | create(Person.class) 60 | .field(Person::getFirstName) 61 | .field(Person::getLastName) 62 | .field(Person::getFavouriteNumber) 63 | .execute(this::openConnection); 64 | 65 | create(Conspiracy.class) 66 | .field(Conspiracy::getName) 67 | .execute(this::openConnection); 68 | 69 | create(relationship(Conspiracy.class, Person.class)) 70 | .fieldLeft(Conspiracy::getName) 71 | .fieldRight(Person::getFirstName) 72 | .fieldRight(Person::getLastName) 73 | .execute(this::openConnection); 74 | 75 | delete(Person.class) 76 | .execute(this::openConnection); 77 | 78 | delete(Conspiracy.class) 79 | .execute(this::openConnection); 80 | 81 | delete(relationship(Conspiracy.class, Person.class)) 82 | .execute(this::openConnection); 83 | 84 | Person smith = new Person("agent","smith"); 85 | smith.setFavouriteNumber(6); 86 | 87 | insert(smith) 88 | .value(Person::getFirstName) 89 | .value(Person::getLastName) 90 | .value(Person::getFavouriteNumber) 91 | .execute(this::openConnection); 92 | 93 | Conspiracy nsa = new Conspiracy("nsa"); 94 | nsa.getMembers().add(smith); 95 | 96 | insert(nsa) 97 | .value(Conspiracy::getName) 98 | .execute(this::openConnection); 99 | 100 | nsa.getMembers().forEach(agent -> { 101 | insert(nsa, agent) 102 | .valueLeft(Conspiracy::getName) 103 | .valueRight(Person::getLastName) 104 | .valueRight(Person::getFirstName) 105 | .execute(this::openConnection); 106 | }); 107 | 108 | Mapper personMapper = Mapper.mapper(Person::new) 109 | .set(Person::setFirstName) 110 | .set(Person::setLastName) 111 | .set(Person::setFavouriteNumber); 112 | 113 | Optional person = from(Person.class) 114 | .where(Person::getLastName) 115 | .equalTo("smith") 116 | .join(relationship(Conspiracy.class, Person.class).invert()) 117 | .using(Person::getFirstName, Person::getLastName) 118 | .join(Conspiracy.class) 119 | .using(Conspiracy::getName) 120 | .where(Conspiracy::getName) 121 | .equalTo("nsa") 122 | .select(personMapper, this::openConnection); 123 | 124 | assertEquals(smith, person.get()); 125 | 126 | delete(relationship(Conspiracy.class, Person.class)) 127 | .whereLeft(Conspiracy::getName) 128 | .equalTo("nsa") 129 | .andRight(Person::getLastName) 130 | .equalTo("smith") 131 | .execute(this::openConnection); 132 | 133 | } 134 | 135 | ``` 136 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | uk.co.benjiweber 8 | benjiql 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | org.apache.maven.plugins 14 | maven-compiler-plugin 15 | 3.1 16 | 17 | 17 18 | 17 19 | 20 | 21 | 22 | 23 | 24 | 25 | junit 26 | junit 27 | 4.12 28 | test 29 | 30 | 31 | org.mockito 32 | mockito-inline 33 | 4.4.0 34 | 35 | 36 | org.hamcrest 37 | hamcrest-all 38 | 1.3 39 | 40 | 41 | org.ow2.asm 42 | asm 43 | 5.0_BETA 44 | 45 | 46 | postgresql 47 | postgresql 48 | 9.1-901.jdbc4 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/ddl/Create.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.ddl; 2 | 3 | import uk.co.benjiweber.benjiql.util.Conventions; 4 | import uk.co.benjiweber.benjiql.mocking.Recorder; 5 | import uk.co.benjiweber.benjiql.mocking.RecordingObject; 6 | 7 | import java.io.Serializable; 8 | import java.sql.Connection; 9 | import java.sql.SQLException; 10 | import java.util.LinkedHashSet; 11 | import java.util.Set; 12 | import java.util.function.Function; 13 | import java.util.function.Supplier; 14 | import java.util.stream.Collectors; 15 | 16 | public class Create { 17 | private final Class cls; 18 | 19 | private final Set fieldNames = new LinkedHashSet<>(); 20 | private final Recorder recorder; 21 | 22 | public Create(Class cls) { 23 | this.cls = cls; 24 | this.recorder = RecordingObject.create(cls); 25 | } 26 | 27 | public static Create create(Class cls) { 28 | return new Create(cls); 29 | } 30 | 31 | public static JoinTables relationship(Class leftTable, Class rightTable) { 32 | return new JoinTables(leftTable, rightTable); 33 | } 34 | 35 | 36 | public static CreateRelationship create(JoinTables joinTables) { 37 | return new CreateRelationship(joinTables.leftTable, joinTables.rightTable); 38 | } 39 | 40 | public Create field(Function getter) { 41 | U result = getter.apply(recorder.getObject()); 42 | String fieldName = recorder.getCurrentPropertyName(); 43 | fieldNames.add(new FieldNameType(fieldName, result.getClass())); 44 | return this; 45 | } 46 | 47 | public String toSql() { 48 | return "CREATE TABLE IF NOT EXISTS " + Conventions.toDbName(cls.getSimpleName()) + " ( " + 49 | fieldNames.stream().map(FieldNameType::toString).collect(Collectors.joining(", ")) 50 | + " ); "; 51 | } 52 | 53 | public void execute(Supplier connectionFactory) throws SQLException { 54 | try(Connection connection = connectionFactory.get()) { 55 | connection.prepareStatement(toSql()).executeUpdate(); 56 | } 57 | } 58 | 59 | static class FieldNameType { 60 | String fieldName; 61 | Class type; 62 | 63 | FieldNameType(String fieldName, Class type) { 64 | this.fieldName = fieldName; 65 | this.type = type; 66 | } 67 | 68 | @Override public String toString() { 69 | return Conventions.toDbName(fieldName) + " " + Conventions.toDbType(type); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/ddl/CreateRelationship.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.ddl; 2 | 3 | import uk.co.benjiweber.benjiql.mocking.Recorder; 4 | import uk.co.benjiweber.benjiql.ddl.Create.FieldNameType; 5 | import uk.co.benjiweber.benjiql.mocking.RecordingObject; 6 | import uk.co.benjiweber.benjiql.util.Conventions; 7 | 8 | import java.io.Serializable; 9 | import java.sql.Connection; 10 | import java.sql.SQLException; 11 | import java.util.LinkedHashSet; 12 | import java.util.Set; 13 | import java.util.function.Function; 14 | import java.util.function.Supplier; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.Stream; 17 | 18 | public class CreateRelationship { 19 | private final Class left; 20 | private final Class right; 21 | 22 | private final Set leftFieldNames = new LinkedHashSet<>(); 23 | private final Set rightFieldNames = new LinkedHashSet<>(); 24 | private final Recorder leftRecorder; 25 | private final Recorder rightRecorder; 26 | 27 | public CreateRelationship(Class leftCls, Class rightCls) { 28 | this.left = leftCls; 29 | this.right = rightCls; 30 | this.leftRecorder = RecordingObject.create(leftCls); 31 | this.rightRecorder = RecordingObject.create(rightCls); 32 | } 33 | 34 | public static Create create(Class cls) { 35 | return new Create(cls); 36 | } 37 | 38 | public CreateRelationship fieldLeft(Function getter) { 39 | V result = getter.apply(leftRecorder.getObject()); 40 | String fieldName = leftRecorder.getCurrentPropertyName(); 41 | leftFieldNames.add(new FieldNameType(Conventions.toDbName(left.getSimpleName()) + "_" + fieldName, result.getClass())); 42 | return this; 43 | } 44 | 45 | public CreateRelationship fieldRight(Function getter) { 46 | V result = getter.apply(rightRecorder.getObject()); 47 | String fieldName = rightRecorder.getCurrentPropertyName(); 48 | rightFieldNames.add(new FieldNameType(Conventions.toDbName(right.getSimpleName()) + "_" + fieldName, result.getClass())); 49 | return this; 50 | } 51 | 52 | public String toSql() { 53 | return "CREATE TABLE IF NOT EXISTS " + Conventions.toDbName(left.getSimpleName() + right.getSimpleName()) + " ( " + 54 | Stream.concat(leftFieldNames.stream(), rightFieldNames.stream()).map(FieldNameType::toString).collect(Collectors.joining(", ")) + 55 | " ); "; 56 | } 57 | 58 | public void execute(Supplier connectionFactory) throws SQLException { 59 | try(Connection connection = connectionFactory.get()) { 60 | connection.prepareStatement(toSql()).executeUpdate(); 61 | } 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/ddl/JoinTables.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.ddl; 2 | 3 | import uk.co.benjiweber.benjiql.util.Conventions; 4 | 5 | public class JoinTables { 6 | public final Class leftTable; 7 | public final Class rightTable; 8 | private final boolean inverted; 9 | 10 | public JoinTables(Class leftTable, Class rightTable) { 11 | this(leftTable, rightTable, false); 12 | } 13 | 14 | public JoinTables(Class leftTable, Class rightTable, boolean inverted) { 15 | this.leftTable = leftTable; 16 | this.rightTable = rightTable; 17 | this.inverted = inverted; 18 | } 19 | 20 | public JoinTables invert() { 21 | return new JoinTables(rightTable, leftTable, true); 22 | } 23 | 24 | public String getName() { 25 | return inverted ? Conventions.toDbName(rightTable, leftTable) : Conventions.toDbName(leftTable, rightTable); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/mocking/DefaultValues.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.mocking; 2 | 3 | import java.util.Map; 4 | 5 | import static java.util.Map.entry; 6 | 7 | public class DefaultValues { 8 | 9 | private static final Map, Object> defaultValues = Map.ofEntries( 10 | entry(String.class, "string"), 11 | entry(Integer.class,0), 12 | entry(Float.class, 0f), 13 | entry(Double.class, 0d), 14 | entry(Long.class, 0L), 15 | entry(Character.class, 'c'), 16 | entry(Byte.class, (byte)0), 17 | entry(int.class, 0), 18 | entry(float.class,0f), 19 | entry(double.class,0d), 20 | entry(long.class, 0L), 21 | entry(char.class, 'c'), 22 | entry(byte.class, (byte)0) 23 | ); 24 | 25 | @SuppressWarnings("unchecked") 26 | public static T getDefault(Class cls) { 27 | return (T) defaultValues.get(cls); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/mocking/Recorder.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.mocking; 2 | 3 | 4 | public class Recorder { 5 | 6 | private T t; 7 | private RecordingObject recorder; 8 | 9 | public Recorder(T t, RecordingObject recorder) { 10 | this.t = t; 11 | this.recorder = recorder; 12 | } 13 | 14 | public String getCurrentPropertyName() { 15 | return recorder.getCurrentPropertyName(); 16 | } 17 | 18 | public T getObject() { 19 | return t; 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/mocking/RecordingObject.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.mocking; 2 | 3 | 4 | import org.mockito.Mockito; 5 | import org.mockito.exceptions.base.MockitoException; 6 | import org.mockito.invocation.InvocationOnMock; 7 | import org.mockito.stubbing.Answer; 8 | import uk.co.benjiweber.benjiql.util.Conventions; 9 | 10 | import java.lang.reflect.Method; 11 | 12 | public class RecordingObject implements Answer { 13 | 14 | private String currentPropertyName = ""; 15 | private Recorder currentMock = null; 16 | 17 | @SuppressWarnings("unchecked") 18 | public static Recorder create(Class cls) { 19 | 20 | final RecordingObject recordingObject = new RecordingObject(); 21 | var mock = Mockito.mock(cls,recordingObject); 22 | return new Recorder((T) mock, recordingObject); 23 | } 24 | 25 | 26 | @Override 27 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 28 | var method = invocationOnMock.getMethod(); 29 | if (method.getName().equals("getCurrentPropertyName")) { 30 | return getCurrentPropertyName(); 31 | } 32 | 33 | currentPropertyName = Conventions.toDbName(method.getName()); 34 | try { 35 | currentMock = create(method.getReturnType()); 36 | return currentMock.getObject(); 37 | } catch (MockitoException e) { 38 | return DefaultValues.getDefault(method.getReturnType()); 39 | } 40 | } 41 | 42 | public String getCurrentPropertyName() { 43 | return currentPropertyName + (currentMock == null ? "" : ("." + currentMock.getCurrentPropertyName())); 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/query/Join.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.query; 2 | 3 | import uk.co.benjiweber.benjiql.util.Conventions; 4 | 5 | import java.io.Serializable; 6 | import java.sql.PreparedStatement; 7 | import java.sql.SQLException; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Optional; 11 | import java.util.function.Function; 12 | 13 | public class Join implements JoinSpecifier { 14 | 15 | private final QueryChain from; 16 | private final Class to; 17 | private List joinConditions = new ArrayList<>(); 18 | 19 | public Join(QueryChain from, Class to) { 20 | this.from = from; 21 | this.to = to; 22 | } 23 | 24 | public Select using(Function p1) { 25 | Select toSelect = new Select<>(to, this); 26 | p1.apply(from.recorder().getObject()); 27 | String fieldName = Conventions.toDbName(from.recorder().getCurrentPropertyName()); 28 | joinConditions.add(new JoinCondition(from.tableName() + "." + from.fieldName(fieldName), toSelect.tableName() + "." + fieldName)); 29 | return toSelect; 30 | } 31 | 32 | public Select using(Function p1, Function p2) { 33 | Select toSelect = using(p1); 34 | p2.apply(from.recorder().getObject()); 35 | String fieldName = Conventions.toDbName(from.recorder().getCurrentPropertyName()); 36 | joinConditions.add(new JoinCondition(from.tableName() + "." + from.fieldName(fieldName), toSelect.tableName() + "." + fieldName)); 37 | return toSelect; 38 | } 39 | 40 | public Optional whereClause() { 41 | return from.whereClause(); 42 | } 43 | 44 | public String fromClause() { 45 | return from.fromClause() + 46 | " JOIN " + 47 | Conventions.toDbName(to) + 48 | " ON " + 49 | joinConditions.stream() 50 | .map(JoinCondition::toSQL) 51 | .reduce((a, b) -> a + " AND " + b) 52 | .orElse(""); 53 | } 54 | 55 | public int setPlaceholders(PreparedStatement stmt) throws SQLException { 56 | return from.setPlaceholders(stmt); 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/query/JoinCondition.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.query; 2 | 3 | public class JoinCondition { 4 | public final String leftFieldName; 5 | public final String rightFieldName; 6 | public final String operator; 7 | 8 | public JoinCondition(String leftFieldName, String rightFieldName) { 9 | this(leftFieldName, rightFieldName, "="); 10 | } 11 | 12 | public JoinCondition(String leftFieldName, String rightFieldName, String operator) { 13 | this.leftFieldName = leftFieldName; 14 | this.rightFieldName = rightFieldName; 15 | this.operator = operator; 16 | } 17 | 18 | public String toSQL() { 19 | return leftFieldName + " " + operator + " " + rightFieldName; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/query/JoinSpecifier.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.query; 2 | 3 | import java.io.Serializable; 4 | import java.util.function.Function; 5 | 6 | public interface JoinSpecifier { 7 | Select using(Function p1); 8 | Select using(Function p1, Function p2); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/query/QueryChain.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.query; 2 | 3 | import uk.co.benjiweber.benjiql.mocking.Recorder; 4 | import uk.co.benjiweber.benjiql.mocking.RecordingObject; 5 | 6 | import java.sql.PreparedStatement; 7 | import java.sql.SQLException; 8 | import java.util.Optional; 9 | 10 | public interface QueryChain { 11 | 12 | String fromClause(); 13 | 14 | Recorder recorder(); 15 | 16 | String tableName(); 17 | 18 | String fieldName(String fieldName); 19 | 20 | Optional whereClause(); 21 | 22 | public int setPlaceholders(PreparedStatement statement) throws SQLException; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/query/RelationshipJoin.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.query; 2 | 3 | import uk.co.benjiweber.benjiql.ddl.JoinTables; 4 | import uk.co.benjiweber.benjiql.mocking.Recorder; 5 | import uk.co.benjiweber.benjiql.mocking.RecordingObject; 6 | import uk.co.benjiweber.benjiql.util.Conventions; 7 | 8 | import java.io.Serializable; 9 | import java.sql.PreparedStatement; 10 | import java.sql.SQLException; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.function.Function; 15 | 16 | public class RelationshipJoin implements RelationshipJoinSpecifier, QueryChain { 17 | private final QueryChain from; 18 | private final JoinTables to; 19 | private List joinConditions = new ArrayList<>(); 20 | private final Recorder recorder; 21 | 22 | public RelationshipJoin(QueryChain from, JoinTables to) { 23 | this.from = from; 24 | this.to = to; 25 | this.recorder = RecordingObject.create(to.rightTable); 26 | } 27 | 28 | @Override 29 | public CanOnlyJoin using(Function p1) { 30 | p1.apply(from.recorder().getObject()); 31 | String fieldName = Conventions.toDbName(from.recorder().getCurrentPropertyName()); 32 | joinConditions.add(new JoinCondition(from.tableName() + "." + fieldName, to.getName() + "." + Conventions.toJoinTableName(to.leftTable, fieldName))); 33 | 34 | 35 | return new CanOnlyJoin() { 36 | public JoinSpecifier join(Class table) { 37 | return new Join<>(RelationshipJoin.this, table); 38 | } 39 | 40 | @Override 41 | public RelationshipJoinSpecifier join(JoinTables table) { 42 | return new RelationshipJoin<>(RelationshipJoin.this, table); 43 | } 44 | }; 45 | } 46 | 47 | @Override 48 | public CanOnlyJoin using(Function p1, Function p2) { 49 | CanOnlyJoin result = using(p1); 50 | p2.apply(from.recorder().getObject()); 51 | String fieldName = Conventions.toDbName(from.recorder().getCurrentPropertyName()); 52 | joinConditions.add(new JoinCondition(from.tableName() + "." + fieldName, to.getName() + "." + Conventions.toJoinTableName(to.leftTable, fieldName))); 53 | return result; 54 | } 55 | 56 | @Override 57 | public String fromClause() { 58 | return from.fromClause() + 59 | " JOIN " + 60 | to.getName() + 61 | " ON " + 62 | joinConditions.stream() 63 | .map(JoinCondition::toSQL) 64 | .reduce((a, b) -> a + " AND " + b) 65 | .orElse(""); 66 | } 67 | 68 | @Override 69 | public Recorder recorder() { 70 | return recorder; 71 | } 72 | 73 | @Override 74 | public String tableName() { 75 | return to.getName(); 76 | } 77 | 78 | @Override 79 | public String fieldName(String fieldName) { 80 | return Conventions.toJoinTableName(to.rightTable, fieldName); 81 | } 82 | 83 | @Override 84 | public Optional whereClause() { 85 | return from.whereClause(); 86 | } 87 | 88 | @Override 89 | public int setPlaceholders(PreparedStatement statement) throws SQLException { 90 | return from.setPlaceholders(statement); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/query/RelationshipJoinSpecifier.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.query; 2 | 3 | import uk.co.benjiweber.benjiql.ddl.JoinTables; 4 | import uk.co.benjiweber.benjiql.util.Conventions; 5 | 6 | import java.io.Serializable; 7 | import java.util.function.Function; 8 | 9 | public interface RelationshipJoinSpecifier { 10 | 11 | CanOnlyJoin using(Function p1); 12 | CanOnlyJoin using(Function p1, Function p2); 13 | 14 | public interface CanOnlyJoin { 15 | public JoinSpecifier join(Class table); 16 | public RelationshipJoinSpecifier join(JoinTables table); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/query/Select.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.query; 2 | 3 | import uk.co.benjiweber.benjiql.ddl.JoinTables; 4 | import uk.co.benjiweber.benjiql.util.Conventions; 5 | import uk.co.benjiweber.benjiql.mocking.Recorder; 6 | import uk.co.benjiweber.benjiql.mocking.RecordingObject; 7 | import uk.co.benjiweber.benjiql.results.Mapper; 8 | import uk.co.benjiweber.benjiql.update.FieldNameValue; 9 | 10 | import java.sql.Connection; 11 | import java.sql.PreparedStatement; 12 | import java.sql.ResultSet; 13 | import java.sql.SQLException; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Optional; 17 | import java.util.function.Function; 18 | import java.util.function.Supplier; 19 | 20 | import static uk.co.benjiweber.benjiql.util.Exceptions.unchecked; 21 | 22 | public class Select implements QueryChain { 23 | private Class cls; 24 | private final Optional join; 25 | final Recorder recorder; 26 | private final List whereFieldNames = new ArrayList<>(); 27 | 28 | public Select(Class cls) { 29 | this.cls = cls; 30 | this.recorder = RecordingObject.create(cls); 31 | this.join = Optional.empty(); 32 | } 33 | 34 | public Select(Class cls, Join join) { 35 | this.cls = cls; 36 | this.join = Optional.of(join); 37 | this.recorder = RecordingObject.create(cls); 38 | } 39 | 40 | public static Select from(Class cls) { 41 | return new Select<>(cls); 42 | } 43 | 44 | public String tableName() { 45 | return Conventions.toDbName(cls); 46 | } 47 | 48 | @Override 49 | public String fieldName(String fieldName) { 50 | return fieldName; 51 | } 52 | 53 | public RelationshipJoinSpecifier join(JoinTables relationship) { 54 | return new RelationshipJoin<>(this, relationship); 55 | } 56 | 57 | public Join join(Class table) { 58 | return new Join(this, table); 59 | } 60 | 61 | public SelectComparison and(Function getter) { 62 | return where(getter); 63 | } 64 | 65 | public SelectComparison where(Function getter) { 66 | getter.apply(recorder.getObject()); 67 | String fieldName = Conventions.toDbName(recorder.getCurrentPropertyName()); 68 | return new SelectComparison() { 69 | public Select equalTo(U value) { 70 | whereFieldNames.add(new FieldNameValue<>(tableName() + "." + fieldName, value, "=")); 71 | return Select.this; 72 | } 73 | 74 | public Select notEqualTo(U value) { 75 | whereFieldNames.add(new FieldNameValue<>(tableName() + "." + fieldName, value, "!=")); 76 | return Select.this; 77 | } 78 | 79 | public Select like(U value) { 80 | whereFieldNames.add(new FieldNameValue<>(tableName() + "." + fieldName, value, "LIKE")); 81 | return Select.this; 82 | } 83 | }; 84 | } 85 | 86 | public String toSql() { 87 | return "SELECT * FROM " + fromClause() + whereClause().map(sql -> " WHERE " + sql).orElse(""); 88 | } 89 | 90 | public Optional whereClause() { 91 | return 92 | whereFieldNames.stream() 93 | .map(FieldNameValue::toSQL) 94 | .reduce((a,b) -> a + " AND " + b) 95 | .map( sql -> 96 | join.map(Join::whereClause) 97 | .orElse(Optional.empty()) 98 | .map(s -> s + " AND ") 99 | .orElse("") 100 | + sql 101 | ); 102 | } 103 | 104 | @Override 105 | public String fromClause() { 106 | return join.map(Join::fromClause).orElse(Conventions.toDbName(cls.getSimpleName())); 107 | } 108 | 109 | @Override 110 | public Recorder recorder() { 111 | return recorder; 112 | } 113 | 114 | public Optional select(Mapper mapper, Supplier connectionFactory) throws SQLException { 115 | try (Connection conn = connectionFactory.get()) { 116 | ResultSet resultSet = runQuery(conn); 117 | return resultSet.next() ? Optional.of(mapper.map(resultSet)) : Optional.empty(); 118 | } 119 | } 120 | 121 | public List list(Mapper mapper, Supplier connectionFactory) throws SQLException { 122 | try (Connection conn = connectionFactory.get()) { 123 | ResultSet resultSet = runQuery(conn); 124 | List results = new ArrayList<>(); 125 | while (resultSet.next()) { 126 | results.add(mapper.map(resultSet)); 127 | } 128 | return results; 129 | } 130 | } 131 | 132 | private ResultSet runQuery(Connection conn) throws SQLException { 133 | PreparedStatement statement = conn.prepareStatement(toSql()); 134 | setPlaceholders(statement); 135 | return statement.executeQuery(); 136 | } 137 | 138 | public int setPlaceholders(PreparedStatement statement) throws SQLException { 139 | int start = join.map(j -> unchecked(() -> j.setPlaceholders(statement))).orElse(0); 140 | for (int i = 0; i < whereFieldNames.size(); i++) { 141 | Conventions.JdbcSetter setter = Conventions.getSetter(whereFieldNames.get(i).value.getClass()); 142 | setter.apply(statement, start + i + 1, whereFieldNames.get(i).value); 143 | } 144 | return whereFieldNames.size() + start; 145 | } 146 | 147 | public interface SelectComparison { 148 | Select equalTo(U value); 149 | Select notEqualTo(U value); 150 | Select like(U value); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/results/ClassMapper.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.results; 2 | 3 | import uk.co.benjiweber.benjiql.util.Conventions; 4 | import uk.co.benjiweber.benjiql.mocking.Recorder; 5 | import uk.co.benjiweber.benjiql.mocking.RecordingObject; 6 | 7 | import java.io.Serializable; 8 | import java.sql.ResultSet; 9 | import java.sql.SQLException; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | import java.util.function.BiConsumer; 13 | import java.util.function.Supplier; 14 | 15 | public class ClassMapper implements Mapper { 16 | private final T t; 17 | private final Set properties = new HashSet<>(); 18 | private final Recorder recorder; 19 | 20 | public ClassMapper(Supplier factory) { 21 | this.t = factory.get(); 22 | recorder = (Recorder)RecordingObject.create(t.getClass()); 23 | } 24 | 25 | public static ClassMapper mapper(Supplier factory) { 26 | return new ClassMapper(factory); 27 | } 28 | 29 | public ClassMapper set(BiConsumer setter) { 30 | this.properties.add(new SettableField(getName(setter), setter)); 31 | return this; 32 | } 33 | 34 | private String getName(BiConsumer setter) { 35 | setter.accept(recorder.getObject(), null); 36 | return Conventions.toDbName(recorder.getCurrentPropertyName()); 37 | } 38 | 39 | public T map(ResultSet resultSet) { 40 | properties.forEach(property -> { 41 | try { 42 | property.setter.accept(t, resultSet.getObject(property.fieldName)); 43 | } catch (SQLException e) { 44 | throw new RuntimeException(e); 45 | } 46 | }); 47 | return t; 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/results/Mapper.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.results; 2 | 3 | import java.sql.ResultSet; 4 | 5 | public interface Mapper { 6 | T map(ResultSet resultSet); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/results/RecordMapper.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.results; 2 | 3 | import uk.co.benjiweber.benjiql.util.Conventions; 4 | import uk.co.benjiweber.benjiql.util.Exceptions; 5 | 6 | import java.io.Serializable; 7 | import java.lang.reflect.RecordComponent; 8 | import java.sql.ResultSet; 9 | import java.sql.SQLException; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Set; 14 | import java.util.function.BiConsumer; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.Stream; 17 | 18 | import static uk.co.benjiweber.benjiql.util.Exceptions.unchecked; 19 | 20 | public class RecordMapper implements Mapper { 21 | private final List properties; 22 | private final Class recordType; 23 | 24 | public RecordMapper(Class recordType) { 25 | this.recordType = recordType; 26 | this.properties = List.of(recordType.getRecordComponents()); 27 | } 28 | 29 | public static RecordMapper mapper(Class cls) { 30 | return new RecordMapper<>(cls); 31 | } 32 | 33 | public T map(ResultSet resultSet) { 34 | Set> constructorParams = new HashSet<>(); 35 | 36 | Object[] propertyValues = properties.stream() 37 | .map(property -> unchecked( 38 | () -> resultSet.getObject( 39 | Conventions.toDbName(property.getName())) 40 | ) 41 | ) 42 | .toArray(); 43 | Class[] propertyTypes = properties.stream().map(property -> property.getType()).collect(Collectors.toList()).toArray(new Class[0]); 44 | 45 | return Exceptions.unchecked(() -> recordType.getConstructor(propertyTypes).newInstance(propertyValues)); 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/results/SettableField.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.results; 2 | 3 | import java.util.function.BiConsumer; 4 | 5 | public class SettableField { 6 | public final String fieldName; 7 | public final BiConsumer setter; 8 | 9 | public SettableField(String fieldName, BiConsumer setter) { 10 | this.fieldName = fieldName; 11 | this.setter = setter; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/update/Delete.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.update; 2 | 3 | import uk.co.benjiweber.benjiql.ddl.JoinTables; 4 | import uk.co.benjiweber.benjiql.util.Conventions; 5 | import uk.co.benjiweber.benjiql.mocking.Recorder; 6 | import uk.co.benjiweber.benjiql.mocking.RecordingObject; 7 | 8 | import java.sql.Connection; 9 | import java.sql.PreparedStatement; 10 | import java.sql.SQLException; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.function.Function; 14 | import java.util.function.Supplier; 15 | import java.util.stream.Collectors; 16 | 17 | public class Delete { 18 | final Class cls; 19 | final Recorder recorder; 20 | final List whereFieldNames = new ArrayList<>(); 21 | 22 | public Delete(Class cls) { 23 | this.cls = cls; 24 | this.recorder = RecordingObject.create(cls); 25 | } 26 | 27 | public static Delete delete(Class cls) { 28 | return new Delete(cls); 29 | } 30 | 31 | public static DeleteFromRelationship delete(JoinTables join) { 32 | return new DeleteFromRelationship(join.leftTable, join.rightTable); 33 | } 34 | 35 | public DeleteComparison and(Function getter) { 36 | return where(getter); 37 | } 38 | 39 | public DeleteComparison where(Function getter) { 40 | getter.apply(recorder.getObject()); 41 | String fieldName = Conventions.toDbName(recorder.getCurrentPropertyName()); 42 | return new DeleteComparison() { 43 | public Delete equalTo(U value) { 44 | whereFieldNames.add(new FieldNameValue(fieldName, value, "=")); 45 | return Delete.this; 46 | } 47 | 48 | public Delete notEqualTo(U value) { 49 | whereFieldNames.add(new FieldNameValue(fieldName, value, "!=")); 50 | return Delete.this; 51 | } 52 | 53 | public Delete like(U value) { 54 | whereFieldNames.add(new FieldNameValue(fieldName, value, "LIKE")); 55 | return Delete.this; 56 | } 57 | }; 58 | } 59 | 60 | public String toSql() { 61 | return "DELETE FROM " + Conventions.toDbName(cls.getSimpleName()) + 62 | (whereFieldNames.size() < 1 ? "" : " WHERE " + whereFieldNames.stream().map(fnv -> fnv.fieldName + " " + fnv.operator + " ?").collect(Collectors.joining(" AND "))); 63 | } 64 | 65 | public void execute(Supplier connectionFactory) throws SQLException { 66 | try (Connection connection = connectionFactory.get()) { 67 | PreparedStatement statement = connection.prepareStatement(toSql()); 68 | for (int i = 0; i < whereFieldNames.size(); i++) { 69 | Conventions.JdbcSetter setter = Conventions.getSetter(whereFieldNames.get(i).value.getClass()); 70 | setter.apply(statement, i + 1, whereFieldNames.get(i).value); 71 | } 72 | statement.executeUpdate(); 73 | } 74 | } 75 | 76 | public interface DeleteComparison { 77 | public Delete equalTo(U value); 78 | public Delete notEqualTo(U value); 79 | public Delete like(U value); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/update/DeleteFromRelationship.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.update; 2 | 3 | import uk.co.benjiweber.benjiql.mocking.Recorder; 4 | import uk.co.benjiweber.benjiql.mocking.RecordingObject; 5 | import uk.co.benjiweber.benjiql.util.Conventions; 6 | 7 | import java.sql.Connection; 8 | import java.sql.PreparedStatement; 9 | import java.sql.SQLException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.function.Function; 13 | import java.util.function.Supplier; 14 | import java.util.stream.Collectors; 15 | import java.util.stream.Stream; 16 | 17 | public class DeleteFromRelationship { 18 | final Class left; 19 | final Class right; 20 | final Recorder leftRecorder; 21 | final Recorder rightRecorder; 22 | final List leftFieldNames = new ArrayList<>(); 23 | final List rightFieldNames = new ArrayList<>(); 24 | 25 | public DeleteFromRelationship(Class left, Class right) { 26 | this.left = left; 27 | this.right = right; 28 | this.leftRecorder = RecordingObject.create(left); 29 | this.rightRecorder = RecordingObject.create(right); 30 | } 31 | 32 | public String toSql() { 33 | List whereFieldNames = Stream.concat(leftFieldNames.stream(), rightFieldNames.stream()).collect(Collectors.toList()); 34 | return "DELETE FROM " + Conventions.toDbName(left, right) + 35 | (whereFieldNames.size() < 1 ? "" : " WHERE " + whereFieldNames.stream().map(fnv -> fnv.fieldName + " " + fnv.operator + " ?").collect(Collectors.joining(" AND "))); 36 | } 37 | 38 | public void execute(Supplier connectionFactory) throws SQLException { 39 | try (Connection connection = connectionFactory.get()) { 40 | PreparedStatement statement = connection.prepareStatement(toSql()); 41 | for (int i = 0; i < leftFieldNames.size(); i++) { 42 | Conventions.JdbcSetter setter = Conventions.getSetter(leftFieldNames.get(i).value.getClass()); 43 | setter.apply(statement, i + 1, leftFieldNames.get(i).value); 44 | } 45 | for (int i = 0; i < rightFieldNames.size(); i++) { 46 | Conventions.JdbcSetter setter = Conventions.getSetter(rightFieldNames.get(i).value.getClass()); 47 | setter.apply(statement, leftFieldNames.size() + i + 1, rightFieldNames.get(i).value); 48 | } 49 | statement.executeUpdate(); 50 | } 51 | } 52 | 53 | public DeleteJoinComparison andLeft(Function getter) { 54 | return whereLeft(getter); 55 | } 56 | 57 | public DeleteJoinComparison whereLeft(Function getter) { 58 | getter.apply(leftRecorder.getObject()); 59 | String fieldName = Conventions.toDbName(leftRecorder.getCurrentPropertyName()); 60 | return new DeleteJoinComparison() { 61 | public DeleteFromRelationship equalTo(V value) { 62 | leftFieldNames.add(new FieldNameValue(Conventions.toJoinTableName(left,fieldName), value, "=")); 63 | return DeleteFromRelationship.this; 64 | } 65 | 66 | public DeleteFromRelationship notEqualTo(V value) { 67 | leftFieldNames.add(new FieldNameValue(Conventions.toJoinTableName(left,fieldName), value, "!=")); 68 | return DeleteFromRelationship.this; 69 | } 70 | 71 | public DeleteFromRelationship like(V value) { 72 | leftFieldNames.add(new FieldNameValue(Conventions.toJoinTableName(left,fieldName), value, "LIKE")); 73 | return DeleteFromRelationship.this; 74 | } 75 | }; 76 | } 77 | 78 | public DeleteJoinComparison andRight(Function getter) { 79 | return whereRight(getter); 80 | } 81 | 82 | public DeleteJoinComparison whereRight(Function getter) { 83 | getter.apply(rightRecorder.getObject()); 84 | String fieldName = Conventions.toDbName(rightRecorder.getCurrentPropertyName()); 85 | return new DeleteJoinComparison() { 86 | public DeleteFromRelationship equalTo(V value) { 87 | rightFieldNames.add(new FieldNameValue(Conventions.toJoinTableName(right,fieldName), value, "=")); 88 | return DeleteFromRelationship.this; 89 | } 90 | 91 | public DeleteFromRelationship notEqualTo(V value) { 92 | rightFieldNames.add(new FieldNameValue(Conventions.toJoinTableName(right,fieldName), value, "!=")); 93 | return DeleteFromRelationship.this; 94 | } 95 | 96 | public DeleteFromRelationship like(V value) { 97 | rightFieldNames.add(new FieldNameValue(Conventions.toJoinTableName(right,fieldName), value, "LIKE")); 98 | return DeleteFromRelationship.this; 99 | } 100 | }; 101 | } 102 | 103 | public interface DeleteJoinComparison { 104 | public DeleteFromRelationship equalTo(V value); 105 | public DeleteFromRelationship notEqualTo(V value); 106 | public DeleteFromRelationship like(V value); 107 | } 108 | 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/update/FieldNameValue.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.update; 2 | 3 | public class FieldNameValue { 4 | public final String fieldName; 5 | public final T value; 6 | public final String operator; 7 | 8 | public FieldNameValue(String fieldName, T value) { 9 | this(fieldName, value, "="); 10 | } 11 | 12 | public FieldNameValue(String fieldName, T value, String operator) { 13 | this.fieldName = fieldName; 14 | this.value = value; 15 | this.operator = operator; 16 | } 17 | 18 | public String toSQL() { 19 | return fieldName + " " + operator + " ?"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/update/InsertRelationship.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.update; 2 | 3 | import uk.co.benjiweber.benjiql.mocking.Recorder; 4 | import uk.co.benjiweber.benjiql.mocking.RecordingObject; 5 | import uk.co.benjiweber.benjiql.util.Conventions; 6 | import uk.co.benjiweber.benjiql.util.ExecutionException; 7 | 8 | import java.io.Serializable; 9 | import java.sql.Connection; 10 | import java.sql.PreparedStatement; 11 | import java.sql.SQLException; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.function.Function; 15 | import java.util.function.Supplier; 16 | import java.util.stream.Collectors; 17 | import java.util.stream.Stream; 18 | 19 | public class InsertRelationship { 20 | private final T leftValue; 21 | private final U rightValue; 22 | 23 | final Recorder leftRecorder; 24 | final Recorder rightRecorder; 25 | final List leftFieldNames = new ArrayList<>(); 26 | final List rightFieldNames = new ArrayList<>(); 27 | 28 | public InsertRelationship(T leftValue, U rightValue) { 29 | this.leftValue = leftValue; 30 | this.rightValue = rightValue; 31 | this.leftRecorder = (Recorder) RecordingObject.create(leftValue.getClass()); 32 | this.rightRecorder = (Recorder) RecordingObject.create(rightValue.getClass()); 33 | } 34 | 35 | public InsertRelationship valueLeft(Function getter) { 36 | V result = getter.apply(leftRecorder.getObject()); 37 | String fieldName = leftRecorder.getCurrentPropertyName(); 38 | leftFieldNames.add(new FieldNameValue(Conventions.toDbName(leftValue.getClass().getSimpleName()) + "_" + fieldName, getter.apply(leftValue))); 39 | return this; 40 | } 41 | 42 | public InsertRelationship valueRight(Function getter) { 43 | V result = getter.apply(rightRecorder.getObject()); 44 | String fieldName = rightRecorder.getCurrentPropertyName(); 45 | rightFieldNames.add(new FieldNameValue(Conventions.toDbName(rightValue.getClass().getSimpleName()) + "_" + fieldName, getter.apply(rightValue))); 46 | return this; 47 | } 48 | 49 | public String toSql() { 50 | List setFieldNames = Stream.concat(leftFieldNames.stream(), rightFieldNames.stream()).collect(Collectors.toList()); 51 | 52 | return "INSERT INTO " + Conventions.toDbName(leftValue.getClass().getSimpleName() + rightValue.getClass().getSimpleName()) 53 | + " (" + setFieldNames.stream().map(fnv -> fnv.fieldName).collect(Collectors.joining(", ")) + ") " 54 | + "VALUES ( " + setFieldNames.stream().map(fnv -> "?").collect(Collectors.joining(", ")) + " )"; 55 | 56 | } 57 | 58 | public void execute(Supplier connectionFactory) { 59 | try { 60 | try (Connection connection = connectionFactory.get()) { 61 | PreparedStatement statement = connection.prepareStatement(toSql()); 62 | for (int i = 0; i < leftFieldNames.size(); i++) { 63 | Conventions.JdbcSetter setter = Conventions.getSetter(leftFieldNames.get(i).value.getClass()); 64 | setter.apply(statement, i + 1, leftFieldNames.get(i).value); 65 | } 66 | for (int i = 0; i < rightFieldNames.size(); i++) { 67 | Conventions.JdbcSetter setter = Conventions.getSetter(rightFieldNames.get(i).value.getClass()); 68 | setter.apply(statement, leftFieldNames.size() + i + 1, rightFieldNames.get(i).value); 69 | } 70 | statement.executeUpdate(); 71 | } 72 | } catch (SQLException e) { 73 | throw new ExecutionException(e); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/update/Upsert.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.update; 2 | 3 | import uk.co.benjiweber.benjiql.util.Conventions; 4 | import uk.co.benjiweber.benjiql.mocking.Recorder; 5 | import uk.co.benjiweber.benjiql.mocking.RecordingObject; 6 | 7 | import java.io.Serializable; 8 | import java.sql.*; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.function.Function; 12 | import java.util.function.Supplier; 13 | import java.util.stream.Collectors; 14 | 15 | public abstract class Upsert { 16 | 17 | final T value; 18 | final Recorder recorder; 19 | final List setFieldNames = new ArrayList<>(); 20 | final List whereFieldNames = new ArrayList<>(); 21 | 22 | public static class Insert extends Upsert { 23 | public Insert(T value) { 24 | super(value); 25 | } 26 | 27 | @Override 28 | public String toSql() { 29 | return "INSERT INTO " + Conventions.toDbName(value.getClass().getSimpleName()) 30 | + " (" + setFieldNames.stream().map(fnv -> fnv.fieldName).collect(Collectors.joining(", ")) + ") " 31 | + "VALUES ( " + setFieldNames.stream().map(fnv -> "?").collect(Collectors.joining(", ")) + " )"; 32 | } 33 | } 34 | 35 | public static class Update extends Upsert { 36 | public Update(T value) { 37 | super(value); 38 | } 39 | 40 | @Override 41 | public String toSql() { 42 | return "UPDATE " + Conventions.toDbName(value.getClass().getSimpleName()) 43 | + " SET " + setFieldNames.stream().map(fnv -> fnv.fieldName + " = ?").collect(Collectors.joining(", ")) 44 | + ((whereFieldNames.size() < 1) ? "" : " WHERE " + whereFieldNames.stream().map(fnv -> fnv.fieldName + " " + fnv.operator + " ?").collect(Collectors.joining(" AND "))); 45 | } 46 | } 47 | 48 | @SuppressWarnings("unchecked") 49 | public Upsert(T value) { 50 | this.value = value; 51 | this.recorder = (Recorder) RecordingObject.create(value.getClass()); 52 | } 53 | 54 | public static Insert insert(T value) { 55 | return new Insert(value); 56 | } 57 | 58 | public static InsertRelationship insert(T leftValue, U rightValue) { 59 | return new InsertRelationship(leftValue, rightValue); 60 | } 61 | 62 | public static Update update(T value) { 63 | return new Update(value); 64 | } 65 | 66 | public Upsert value(Function getter) { 67 | U result = getter.apply(recorder.getObject()); 68 | String fieldName = recorder.getCurrentPropertyName(); 69 | setFieldNames.add(new FieldNameValue(fieldName, getter.apply(value))); 70 | return this; 71 | } 72 | 73 | public abstract String toSql(); 74 | 75 | public void execute(Supplier connectionFactory) throws SQLException { 76 | try (Connection connection = connectionFactory.get()) { 77 | PreparedStatement statement = connection.prepareStatement(toSql()); 78 | for (int i = 0; i < setFieldNames.size(); i++) { 79 | Conventions.JdbcSetter setter = Conventions.getSetter(setFieldNames.get(i).value.getClass()); 80 | setter.apply(statement, i + 1, setFieldNames.get(i).value); 81 | } 82 | for (int i = 0; i < whereFieldNames.size(); i++) { 83 | Conventions.JdbcSetter setter = Conventions.getSetter(whereFieldNames.get(i).value.getClass()); 84 | setter.apply(statement, i + 1 + setFieldNames.size(), whereFieldNames.get(i).value); 85 | } 86 | statement.executeUpdate(); 87 | } 88 | } 89 | 90 | public UpdateComparison and(Function getter) { 91 | return where(getter); 92 | } 93 | 94 | public UpdateComparison where(Function getter) { 95 | getter.apply(recorder.getObject()); 96 | String fieldName = Conventions.toDbName(recorder.getCurrentPropertyName()); 97 | return new UpdateComparison(){ 98 | public Upsert equalTo(U value) { 99 | whereFieldNames.add(new FieldNameValue(fieldName, value, "=")); 100 | return Upsert.this; 101 | } 102 | public Upsert notEqualTo(U value) { 103 | whereFieldNames.add(new FieldNameValue(fieldName, value, "!=")); 104 | return Upsert.this; 105 | } 106 | public Upsert like(U value) { 107 | whereFieldNames.add(new FieldNameValue(fieldName, value, "LIKE")); 108 | return Upsert.this; 109 | } 110 | }; 111 | } 112 | 113 | public interface UpdateComparison { 114 | public Upsert equalTo(U value); 115 | public Upsert notEqualTo(U value); 116 | public Upsert like(U value); 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/util/Conventions.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.util; 2 | 3 | 4 | import java.sql.JDBCType; 5 | import java.sql.PreparedStatement; 6 | import java.sql.SQLException; 7 | import java.util.Arrays; 8 | import static java.util.Map.entry; 9 | import java.util.Map; 10 | 11 | public class Conventions { 12 | 13 | private static final Map, String> dbTypes = Map.ofEntries( 14 | entry(String.class, "text"), 15 | entry(Integer.class, "integer"), 16 | entry(Float.class, "double"), 17 | entry(Double.class, "double"), 18 | entry(Long.class, "bigint"), 19 | entry(Character.class, "integer"), 20 | entry(Byte.class, "integer"), 21 | entry(int.class, "integer"), 22 | entry(float.class, "double"), 23 | entry(double.class, "double"), 24 | entry(long.class, "bigint"), 25 | entry(char.class, "integer"), 26 | entry(byte.class, "integer") 27 | ); 28 | 29 | private static final Map, JDBCType> jdbcTypes = Map.ofEntries( 30 | entry(String.class, JDBCType.VARCHAR), 31 | entry(Integer.class, JDBCType.INTEGER), 32 | entry(Float.class, JDBCType.FLOAT), 33 | entry(Double.class, JDBCType.DOUBLE), 34 | entry(Long.class, JDBCType.BIGINT), 35 | entry(Character.class, JDBCType.CHAR), 36 | entry(Byte.class, JDBCType.TINYINT), 37 | entry(int.class, JDBCType.INTEGER), 38 | entry(float.class, JDBCType.FLOAT), 39 | entry(double.class, JDBCType.DOUBLE), 40 | entry(long.class, JDBCType.BIGINT), 41 | entry(char.class, JDBCType.CHAR), 42 | entry(byte.class, JDBCType.TINYINT) 43 | ); 44 | 45 | public interface JdbcSetter { 46 | public void apply(PreparedStatement stmt, int index, T t) throws SQLException; 47 | } 48 | private static final Map, JdbcSetter> jdbcSetters = Map.ofEntries( 49 | entry(String.class, (JdbcSetter)PreparedStatement::setString), 50 | entry(Integer.class, (JdbcSetter)PreparedStatement::setInt), 51 | entry(Float.class, (JdbcSetter)PreparedStatement::setFloat), 52 | entry(Double.class, (JdbcSetter) PreparedStatement::setDouble), 53 | entry(Long.class, (JdbcSetter)PreparedStatement::setLong), 54 | entry(Character.class, (JdbcSetter)PreparedStatement::setInt), 55 | entry(Byte.class, (JdbcSetter)PreparedStatement::setInt), 56 | entry(int.class, (JdbcSetter)PreparedStatement::setInt), 57 | entry(float.class, (JdbcSetter)PreparedStatement::setFloat), 58 | entry(double.class, (JdbcSetter)PreparedStatement::setDouble), 59 | entry(long.class, (JdbcSetter)PreparedStatement::setLong), 60 | entry(char.class, (JdbcSetter)PreparedStatement::setInt), 61 | entry(byte.class, (JdbcSetter)PreparedStatement::setInt) 62 | ); 63 | 64 | public static String toDbName(String name) { 65 | return uncapitalize(toSnakeCase(banishGetterSetters(name))); 66 | } 67 | 68 | public static String toDbName(Class... clses) { 69 | return Arrays.asList(clses) 70 | .stream() 71 | .map(Class::getSimpleName) 72 | .map(Conventions::toDbName) 73 | .reduce((a,b) -> a + "_" + b) 74 | .orElse(""); 75 | } 76 | 77 | public static String toJoinTableName(Class cls, String name) { 78 | return toDbName(cls.getSimpleName()) + "_" + toDbName(name); 79 | } 80 | 81 | private static String banishGetterSetters(String name) { 82 | return name.replaceAll("^(get|set)", ""); 83 | } 84 | 85 | public static String uncapitalize(String s) { 86 | return Character.toLowerCase(s.charAt(0)) + s.substring(1); 87 | } 88 | 89 | public static String toSnakeCase(String s) { 90 | return s.replaceAll("([a-z])([A-Z])","$1_$2").toLowerCase(); 91 | } 92 | 93 | public static String toDbType(Class cls) { 94 | return dbTypes.getOrDefault(cls, "text"); 95 | } 96 | 97 | public static JdbcSetter getSetter(Class cls) { 98 | return jdbcSetters.get(cls); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/util/Exceptions.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.util; 2 | 3 | public class Exceptions { 4 | public interface ExceptionalSupplier { 5 | public R apply() throws Exception; 6 | } 7 | 8 | public interface ExceptionalVoid { 9 | public void apply() throws Exception; 10 | } 11 | 12 | public static R unchecked(ExceptionalSupplier f) { 13 | try { 14 | return f.apply(); 15 | } catch (RuntimeException | Error e) { 16 | throw e; 17 | } catch (Exception e) { 18 | throw new RuntimeException(e); 19 | } 20 | } 21 | 22 | public static void unchecked(ExceptionalVoid f) { 23 | try { 24 | f.apply(); 25 | } catch (RuntimeException | Error e) { 26 | throw e; 27 | } catch (Exception e) { 28 | throw new RuntimeException(e); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/uk/co/benjiweber/benjiql/util/ExecutionException.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.util; 2 | 3 | public class ExecutionException extends RuntimeException { 4 | public ExecutionException(Throwable e) { 5 | super(e); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/uk/co/benjiweber/benjiql/ddl/CreateTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.ddl; 2 | 3 | import org.junit.Test; 4 | import uk.co.benjiweber.benjiql.example.Conspiracy; 5 | import uk.co.benjiweber.benjiql.example.Person; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | import static uk.co.benjiweber.benjiql.ddl.Create.create; 9 | import static uk.co.benjiweber.benjiql.ddl.Create.relationship; 10 | 11 | public class CreateTest { 12 | static class Address { 13 | public String getFirstLine() { return null; } 14 | public Integer getHouseNumber() { return null; } 15 | } 16 | 17 | @Test 18 | public void check_create_matches_example() throws Exception { 19 | String sql = create(Address.class) 20 | .field(Address::getFirstLine) 21 | .field(Address::getHouseNumber) 22 | .toSql(); 23 | 24 | assertEquals("CREATE TABLE IF NOT EXISTS address ( first_line text, house_number integer );", sql.trim()); 25 | 26 | } 27 | 28 | @Test 29 | public void example_join_table() { 30 | String sql = create(relationship(Conspiracy.class, Person.class)) 31 | .fieldLeft(Conspiracy::getName) 32 | .fieldRight(Person::getFirstName) 33 | .fieldRight(Person::getLastName) 34 | .toSql(); 35 | 36 | assertEquals("CREATE TABLE IF NOT EXISTS conspiracy_person ( conspiracy_name text, person_first_name text, person_last_name text ); ", sql); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/uk/co/benjiweber/benjiql/example/Conspiracy.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.example; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | public class Conspiracy { 7 | private Set members = new HashSet<>(); 8 | private String name; 9 | 10 | public Conspiracy() {} 11 | 12 | public Conspiracy(String name) { 13 | this.name = name; 14 | } 15 | 16 | public Set getMembers() { 17 | return members; 18 | } 19 | 20 | public void setMembers(Set members) { 21 | this.members = members; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public void setName(String name) { 29 | this.name = name; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/uk/co/benjiweber/benjiql/example/Person.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.example; 2 | 3 | public class Person { 4 | private String firstName; 5 | private String lastName; 6 | private Integer favouriteNumber; 7 | 8 | public Person(String firstName, String lastName) { 9 | this.firstName = firstName; 10 | this.lastName = lastName; 11 | } 12 | 13 | public Person() {} 14 | 15 | public String getFirstName() { 16 | return firstName; 17 | } 18 | 19 | public void setFirstName(String firstName) { 20 | this.firstName = firstName; 21 | } 22 | 23 | public String getLastName() { 24 | return lastName; 25 | } 26 | 27 | public void setLastName(String lastName) { 28 | this.lastName = lastName; 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "Person{" + 34 | "firstName='" + firstName + '\'' + 35 | ", lastName='" + lastName + '\'' + 36 | '}'; 37 | } 38 | 39 | public Integer getFavouriteNumber() { 40 | return favouriteNumber; 41 | } 42 | 43 | public void setFavouriteNumber(Integer favouriteNumber) { 44 | this.favouriteNumber = favouriteNumber; 45 | } 46 | 47 | @Override 48 | public boolean equals(Object o) { 49 | if (this == o) return true; 50 | if (o == null || getClass() != o.getClass()) return false; 51 | 52 | Person person = (Person) o; 53 | 54 | if (favouriteNumber != null ? !favouriteNumber.equals(person.favouriteNumber) : person.favouriteNumber != null) 55 | return false; 56 | if (firstName != null ? !firstName.equals(person.firstName) : person.firstName != null) return false; 57 | if (lastName != null ? !lastName.equals(person.lastName) : person.lastName != null) return false; 58 | 59 | return true; 60 | } 61 | 62 | @Override 63 | public int hashCode() { 64 | int result = firstName != null ? firstName.hashCode() : 0; 65 | result = 31 * result + (lastName != null ? lastName.hashCode() : 0); 66 | result = 31 * result + (favouriteNumber != null ? favouriteNumber.hashCode() : 0); 67 | return result; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/uk/co/benjiweber/benjiql/example/RealExample.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.example; 2 | 3 | import org.junit.Test; 4 | import uk.co.benjiweber.benjiql.results.ClassMapper; 5 | import uk.co.benjiweber.benjiql.results.Mapper; 6 | 7 | import java.sql.Connection; 8 | import java.sql.DriverManager; 9 | import java.sql.SQLException; 10 | import java.util.Optional; 11 | import java.util.Properties; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | import static uk.co.benjiweber.benjiql.ddl.Create.create; 15 | import static uk.co.benjiweber.benjiql.ddl.Create.relationship; 16 | import static uk.co.benjiweber.benjiql.query.Select.from; 17 | import static uk.co.benjiweber.benjiql.update.Delete.delete; 18 | import static uk.co.benjiweber.benjiql.update.Upsert.insert; 19 | import static uk.co.benjiweber.benjiql.update.Upsert.update; 20 | 21 | public class RealExample { 22 | 23 | @Test public void example_of_create_table_persist_retrieve_and_update_with_real_database() throws SQLException { 24 | create(Person.class) 25 | .field(Person::getFirstName) 26 | .field(Person::getLastName) 27 | .field(Person::getFavouriteNumber) 28 | .execute(this::openConnection); 29 | 30 | delete(Person.class) 31 | .where(Person::getLastName) 32 | .equalTo("weber") 33 | .execute(this::openConnection); 34 | 35 | Person benji = new Person("benji","weber"); 36 | benji.setFavouriteNumber(9); 37 | 38 | insert(benji) 39 | .value(Person::getFirstName) 40 | .value(Person::getLastName) 41 | .value(Person::getFavouriteNumber) 42 | .execute(this::openConnection); 43 | 44 | benji.setFirstName("benji-updated"); 45 | 46 | update(benji) 47 | .value(Person::getFirstName) 48 | .where(Person::getLastName) 49 | .equalTo("weber") 50 | .execute(this::openConnection); 51 | 52 | Mapper personMapper = ClassMapper.mapper(Person::new) 53 | .set(Person::setFirstName) 54 | .set(Person::setLastName) 55 | .set(Person::setFavouriteNumber); 56 | 57 | Optional result = from(Person.class) 58 | .where(Person::getFirstName) 59 | .like("%updated") 60 | .and(Person::getLastName) 61 | .equalTo("weber") 62 | .select(personMapper, this::openConnection); 63 | 64 | assertEquals("benji-updated", result.get().getFirstName()); 65 | assertEquals("weber", result.get().getLastName()); 66 | assertEquals((Integer)9, result.get().getFavouriteNumber()); 67 | } 68 | 69 | @Test public void example_of_select_with_join() throws SQLException { 70 | create(Person.class) 71 | .field(Person::getFirstName) 72 | .field(Person::getLastName) 73 | .field(Person::getFavouriteNumber) 74 | .execute(this::openConnection); 75 | 76 | create(Conspiracy.class) 77 | .field(Conspiracy::getName) 78 | .execute(this::openConnection); 79 | 80 | create(relationship(Conspiracy.class, Person.class)) 81 | .fieldLeft(Conspiracy::getName) 82 | .fieldRight(Person::getFirstName) 83 | .fieldRight(Person::getLastName) 84 | .execute(this::openConnection); 85 | 86 | delete(Person.class) 87 | .execute(this::openConnection); 88 | 89 | delete(Conspiracy.class) 90 | .execute(this::openConnection); 91 | 92 | delete(relationship(Conspiracy.class, Person.class)) 93 | .execute(this::openConnection); 94 | 95 | Person smith = new Person("agent","smith"); 96 | smith.setFavouriteNumber(6); 97 | 98 | insert(smith) 99 | .value(Person::getFirstName) 100 | .value(Person::getLastName) 101 | .value(Person::getFavouriteNumber) 102 | .execute(this::openConnection); 103 | 104 | Conspiracy nsa = new Conspiracy("nsa"); 105 | nsa.getMembers().add(smith); 106 | 107 | insert(nsa) 108 | .value(Conspiracy::getName) 109 | .execute(this::openConnection); 110 | 111 | nsa.getMembers().forEach(agent -> { 112 | insert(nsa, agent) 113 | .valueLeft(Conspiracy::getName) 114 | .valueRight(Person::getLastName) 115 | .valueRight(Person::getFirstName) 116 | .execute(this::openConnection); 117 | }); 118 | 119 | Mapper personMapper = ClassMapper.mapper(Person::new) 120 | .set(Person::setFirstName) 121 | .set(Person::setLastName) 122 | .set(Person::setFavouriteNumber); 123 | 124 | Optional person = from(Person.class) 125 | .where(Person::getLastName) 126 | .equalTo("smith") 127 | .join(relationship(Conspiracy.class, Person.class).invert()) 128 | .using(Person::getFirstName, Person::getLastName) 129 | .join(Conspiracy.class) 130 | .using(Conspiracy::getName) 131 | .where(Conspiracy::getName) 132 | .equalTo("nsa") 133 | .select(personMapper, this::openConnection); 134 | 135 | assertEquals(smith, person.get()); 136 | 137 | delete(relationship(Conspiracy.class, Person.class)) 138 | .whereLeft(Conspiracy::getName) 139 | .equalTo("nsa") 140 | .andRight(Person::getLastName) 141 | .equalTo("smith") 142 | .execute(this::openConnection); 143 | 144 | } 145 | 146 | 147 | private Connection openConnection() { 148 | try { 149 | Properties connectionProps = new Properties(); 150 | connectionProps.put("user", "benjiql"); 151 | connectionProps.put("password", "benjiql"); 152 | return DriverManager.getConnection("jdbc:postgresql:benjiql", connectionProps); 153 | } catch (SQLException e) { 154 | throw new RuntimeException(e); 155 | } 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/test/java/uk/co/benjiweber/benjiql/example/RealExampleWithRecords.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.example; 2 | 3 | import org.junit.Test; 4 | import uk.co.benjiweber.benjiql.results.Mapper; 5 | import uk.co.benjiweber.benjiql.results.RecordMapper; 6 | 7 | import java.sql.Connection; 8 | import java.sql.DriverManager; 9 | import java.sql.SQLException; 10 | import java.util.HashSet; 11 | import java.util.Optional; 12 | import java.util.Properties; 13 | import java.util.Set; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static uk.co.benjiweber.benjiql.ddl.Create.create; 17 | import static uk.co.benjiweber.benjiql.ddl.Create.relationship; 18 | import static uk.co.benjiweber.benjiql.query.Select.from; 19 | import static uk.co.benjiweber.benjiql.results.RecordMapper.mapper; 20 | import static uk.co.benjiweber.benjiql.update.Delete.delete; 21 | import static uk.co.benjiweber.benjiql.update.Upsert.insert; 22 | import static uk.co.benjiweber.benjiql.update.Upsert.update; 23 | 24 | public class RealExampleWithRecords { 25 | public record Person (String firstName, String lastName, Integer favouriteNumber) {} 26 | public record Conspiracy (Set members, String name) { 27 | public Conspiracy(String name) { 28 | this(new HashSet<>(), name); 29 | } 30 | } 31 | 32 | @Test public void example_of_create_table_persist_retrieve_and_update_with_real_database() throws SQLException { 33 | create(Person.class) 34 | .field(Person::firstName) 35 | .field(Person::lastName) 36 | .field(Person::favouriteNumber) 37 | .execute(this::openConnection); 38 | 39 | delete(Person.class) 40 | .where(Person::lastName) 41 | .equalTo("weber") 42 | .execute(this::openConnection); 43 | 44 | Person benji = new Person("benji","weber",9); 45 | 46 | insert(benji) 47 | .value(Person::firstName) 48 | .value(Person::lastName) 49 | .value(Person::favouriteNumber) 50 | .execute(this::openConnection); 51 | 52 | benji = new Person("benji-updated", "weber", 9); 53 | 54 | update(benji) 55 | .value(Person::firstName) 56 | .where(Person::lastName) 57 | .equalTo("weber") 58 | .execute(this::openConnection); 59 | 60 | Optional result = from(Person.class) 61 | .where(Person::firstName) 62 | .like("%updated") 63 | .and(Person::lastName) 64 | .equalTo("weber") 65 | .select(mapper(Person.class), this::openConnection); 66 | 67 | assertEquals("benji-updated", result.get().firstName()); 68 | assertEquals("weber", result.get().lastName()); 69 | assertEquals((Integer)9, result.get().favouriteNumber()); 70 | } 71 | 72 | @Test public void example_of_select_with_join() throws SQLException { 73 | create(Person.class) 74 | .field(Person::firstName) 75 | .field(Person::lastName) 76 | .field(Person::favouriteNumber) 77 | .execute(this::openConnection); 78 | 79 | create(Conspiracy.class) 80 | .field(Conspiracy::name) 81 | .execute(this::openConnection); 82 | 83 | create(relationship(Conspiracy.class, Person.class)) 84 | .fieldLeft(Conspiracy::name) 85 | .fieldRight(Person::firstName) 86 | .fieldRight(Person::lastName) 87 | .execute(this::openConnection); 88 | 89 | delete(Person.class) 90 | .execute(this::openConnection); 91 | 92 | delete(Conspiracy.class) 93 | .execute(this::openConnection); 94 | 95 | delete(relationship(Conspiracy.class, Person.class)) 96 | .execute(this::openConnection); 97 | 98 | Person smith = new Person("agent","smith", 6); 99 | 100 | insert(smith) 101 | .value(Person::firstName) 102 | .value(Person::lastName) 103 | .value(Person::favouriteNumber) 104 | .execute(this::openConnection); 105 | 106 | Conspiracy nsa = new Conspiracy("nsa"); 107 | nsa.members().add(smith); 108 | 109 | insert(nsa) 110 | .value(Conspiracy::name) 111 | .execute(this::openConnection); 112 | 113 | nsa.members().forEach(agent -> { 114 | insert(nsa, agent) 115 | .valueLeft(Conspiracy::name) 116 | .valueRight(Person::lastName) 117 | .valueRight(Person::firstName) 118 | .execute(this::openConnection); 119 | }); 120 | 121 | Optional person = from(Person.class) 122 | .where(Person::lastName) 123 | .equalTo("smith") 124 | .join(relationship(Conspiracy.class, Person.class).invert()) 125 | .using(Person::firstName, Person::lastName) 126 | .join(Conspiracy.class) 127 | .using(Conspiracy::name) 128 | .where(Conspiracy::name) 129 | .equalTo("nsa") 130 | .select(mapper(Person.class), this::openConnection); 131 | 132 | assertEquals(smith, person.get()); 133 | 134 | delete(relationship(Conspiracy.class, Person.class)) 135 | .whereLeft(Conspiracy::name) 136 | .equalTo("nsa") 137 | .andRight(Person::lastName) 138 | .equalTo("smith") 139 | .execute(this::openConnection); 140 | 141 | } 142 | 143 | 144 | private Connection openConnection() { 145 | try { 146 | Properties connectionProps = new Properties(); 147 | connectionProps.put("user", "benjiql"); 148 | connectionProps.put("password", "benjiql"); 149 | return DriverManager.getConnection("jdbc:postgresql:benjiql", connectionProps); 150 | } catch (SQLException e) { 151 | throw new RuntimeException(e); 152 | } 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/test/java/uk/co/benjiweber/benjiql/query/SelectRecordTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.query; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.Mock; 6 | import org.mockito.junit.MockitoJUnitRunner; 7 | import uk.co.benjiweber.benjiql.results.Mapper; 8 | import uk.co.benjiweber.benjiql.results.RecordMapper; 9 | 10 | import java.sql.Connection; 11 | import java.sql.PreparedStatement; 12 | import java.sql.ResultSet; 13 | import java.sql.SQLException; 14 | import java.util.List; 15 | import java.util.Optional; 16 | import java.util.Set; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.mockito.ArgumentMatchers.any; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | import static uk.co.benjiweber.benjiql.ddl.Create.relationship; 23 | import static uk.co.benjiweber.benjiql.query.Select.from; 24 | 25 | @RunWith(MockitoJUnitRunner.class) 26 | public class SelectRecordTest { 27 | 28 | public record Person (String firstName, String lastName, Integer favouriteNumber) {} 29 | public record Conspiracy (Set members, String name) {} 30 | 31 | 32 | @Mock Connection mockConnection; 33 | @Mock PreparedStatement mockStatement; 34 | @Mock ResultSet mockResults; 35 | Mapper personMapper = new RecordMapper(Person.class); 36 | 37 | @Test public void should_match_example() { 38 | String sql = from(Person.class) 39 | .where(Person::firstName) 40 | .equalTo("benji") 41 | .and(Person::lastName) 42 | .notEqualTo("foo") 43 | .and(Person::lastName) 44 | .like("web%") 45 | .and(Person::favouriteNumber) 46 | .equalTo(5) 47 | .toSql(); 48 | 49 | assertEquals("SELECT * FROM person WHERE person.first_name = ? AND person.last_name != ? AND person.last_name LIKE ? AND person.favourite_number = ?", sql.trim()); 50 | } 51 | 52 | @Test public void should_allow_joins() { 53 | String sql = from(Person.class) 54 | .where(Person::lastName) 55 | .equalTo("smith") 56 | .join(relationship(Conspiracy.class, Person.class).invert()) 57 | .using(Person::firstName, Person::lastName) 58 | .join(Conspiracy.class) 59 | .using(Conspiracy::name) 60 | .where(Conspiracy::name) 61 | .equalTo("nsa") 62 | .toSql(); 63 | 64 | assertEquals( 65 | "SELECT * FROM person " + 66 | "JOIN conspiracy_person " + 67 | "ON person.first_name = conspiracy_person.person_first_name " + 68 | "AND person.last_name = conspiracy_person.person_last_name " + 69 | "JOIN conspiracy " + 70 | "ON conspiracy_person.conspiracy_name = conspiracy.name " + 71 | "WHERE person.last_name = ? " + 72 | "AND conspiracy.name = ?", 73 | 74 | sql 75 | ); 76 | } 77 | 78 | @Test public void should_set_values() throws SQLException { 79 | when(mockConnection.prepareStatement(any(String.class))).thenReturn(mockStatement); 80 | when(mockStatement.executeQuery()).thenReturn(mockResults); 81 | 82 | Optional result = from(Person.class) 83 | .where(Person::firstName) 84 | .equalTo("benji") 85 | .and(Person::lastName) 86 | .notEqualTo("foo") 87 | .and(Person::lastName) 88 | .like("web%") 89 | .and(Person::favouriteNumber) 90 | .equalTo(5) 91 | .select(personMapper, () -> mockConnection); 92 | 93 | verify(mockStatement).setString(1,"benji"); 94 | verify(mockStatement).setString(2,"foo"); 95 | verify(mockStatement).setString(3,"web%"); 96 | verify(mockStatement).setInt(4, 5); 97 | } 98 | 99 | @Test public void should_map_results() throws SQLException { 100 | when(mockConnection.prepareStatement(any(String.class))).thenReturn(mockStatement); 101 | when(mockStatement.executeQuery()).thenReturn(mockResults); 102 | when(mockResults.next()).thenReturn(true); 103 | when(mockResults.getObject("first_name")).thenReturn("fname"); 104 | when(mockResults.getObject("last_name")).thenReturn("lname"); 105 | when(mockResults.getObject("favourite_number")).thenReturn(9001); 106 | 107 | Optional result = from(Person.class) 108 | .where(Person::firstName) 109 | .equalTo("benji") 110 | .and(Person::lastName) 111 | .notEqualTo("foo") 112 | .and(Person::lastName) 113 | .like("web%") 114 | .and(Person::favouriteNumber) 115 | .equalTo(5) 116 | .select(personMapper, () -> mockConnection); 117 | 118 | assertEquals("fname", result.get().firstName()); 119 | assertEquals("lname", result.get().lastName()); 120 | assertEquals((Integer)9001, result.get().favouriteNumber()); 121 | } 122 | 123 | @Test public void should_map_results_list() throws SQLException { 124 | when(mockConnection.prepareStatement(any(String.class))).thenReturn(mockStatement); 125 | when(mockStatement.executeQuery()).thenReturn(mockResults); 126 | when(mockResults.next()).thenReturn(true).thenReturn(false); 127 | when(mockResults.getObject("first_name")).thenReturn("fname"); 128 | when(mockResults.getObject("last_name")).thenReturn("lname"); 129 | when(mockResults.getObject("favourite_number")).thenReturn(9001); 130 | 131 | List result = from(Person.class) 132 | .where(Person::firstName) 133 | .equalTo("benji") 134 | .and(Person::lastName) 135 | .notEqualTo("foo") 136 | .and(Person::lastName) 137 | .like("web%") 138 | .and(Person::favouriteNumber) 139 | .equalTo(5) 140 | .list(personMapper, () -> mockConnection); 141 | 142 | assertEquals(1, result.size()); 143 | assertEquals("fname", result.get(0).firstName()); 144 | assertEquals("lname", result.get(0).lastName()); 145 | assertEquals((Integer)9001, result.get(0).favouriteNumber()); 146 | } 147 | 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/test/java/uk/co/benjiweber/benjiql/query/SelectTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.query; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.MockitoJUnitRunner; 8 | import uk.co.benjiweber.benjiql.example.Conspiracy; 9 | import uk.co.benjiweber.benjiql.example.Person; 10 | import uk.co.benjiweber.benjiql.results.Mapper; 11 | 12 | import java.sql.*; 13 | import java.util.List; 14 | import java.util.Optional; 15 | 16 | import static org.junit.Assert.assertEquals; 17 | import static org.mockito.ArgumentMatchers.any; 18 | import static org.mockito.Mockito.verify; 19 | import static org.mockito.Mockito.when; 20 | import static uk.co.benjiweber.benjiql.ddl.Create.relationship; 21 | import static uk.co.benjiweber.benjiql.query.Select.from; 22 | import static uk.co.benjiweber.benjiql.results.ClassMapper.mapper; 23 | 24 | @RunWith(MockitoJUnitRunner.class) 25 | public class SelectTest { 26 | 27 | @Mock Connection mockConnection; 28 | @Mock PreparedStatement mockStatement; 29 | @Mock ResultSet mockResults; 30 | Mapper personMapper = mapper(Person::new).set(Person::setFirstName).set(Person::setLastName).set(Person::setFavouriteNumber); 31 | 32 | @Test public void should_match_example() { 33 | String sql = from(Person.class) 34 | .where(Person::getFirstName) 35 | .equalTo("benji") 36 | .and(Person::getLastName) 37 | .notEqualTo("foo") 38 | .and(Person::getLastName) 39 | .like("web%") 40 | .and(Person::getFavouriteNumber) 41 | .equalTo(5) 42 | .toSql(); 43 | 44 | assertEquals("SELECT * FROM person WHERE person.first_name = ? AND person.last_name != ? AND person.last_name LIKE ? AND person.favourite_number = ?", sql.trim()); 45 | } 46 | 47 | @Test public void should_allow_joins() { 48 | String sql = from(Person.class) 49 | .where(Person::getLastName) 50 | .equalTo("smith") 51 | .join(relationship(Conspiracy.class, Person.class).invert()) 52 | .using(Person::getFirstName, Person::getLastName) 53 | .join(Conspiracy.class) 54 | .using(Conspiracy::getName) 55 | .where(Conspiracy::getName) 56 | .equalTo("nsa") 57 | .toSql(); 58 | 59 | assertEquals( 60 | "SELECT * FROM person " + 61 | "JOIN conspiracy_person " + 62 | "ON person.first_name = conspiracy_person.person_first_name " + 63 | "AND person.last_name = conspiracy_person.person_last_name " + 64 | "JOIN conspiracy " + 65 | "ON conspiracy_person.conspiracy_name = conspiracy.name " + 66 | "WHERE person.last_name = ? " + 67 | "AND conspiracy.name = ?", 68 | 69 | sql 70 | ); 71 | } 72 | 73 | @Test public void should_set_values() throws SQLException { 74 | when(mockConnection.prepareStatement(any(String.class))).thenReturn(mockStatement); 75 | when(mockStatement.executeQuery()).thenReturn(mockResults); 76 | 77 | Optional result = from(Person.class) 78 | .where(Person::getFirstName) 79 | .equalTo("benji") 80 | .and(Person::getLastName) 81 | .notEqualTo("foo") 82 | .and(Person::getLastName) 83 | .like("web%") 84 | .and(Person::getFavouriteNumber) 85 | .equalTo(5) 86 | .select(personMapper, () -> mockConnection); 87 | 88 | verify(mockStatement).setString(1,"benji"); 89 | verify(mockStatement).setString(2,"foo"); 90 | verify(mockStatement).setString(3,"web%"); 91 | verify(mockStatement).setInt(4, 5); 92 | } 93 | 94 | @Test public void should_map_results() throws SQLException { 95 | when(mockConnection.prepareStatement(any(String.class))).thenReturn(mockStatement); 96 | when(mockStatement.executeQuery()).thenReturn(mockResults); 97 | when(mockResults.next()).thenReturn(true); 98 | when(mockResults.getObject("first_name")).thenReturn("fname"); 99 | when(mockResults.getObject("last_name")).thenReturn("lname"); 100 | when(mockResults.getObject("favourite_number")).thenReturn(9001); 101 | 102 | Optional result = from(Person.class) 103 | .where(Person::getFirstName) 104 | .equalTo("benji") 105 | .and(Person::getLastName) 106 | .notEqualTo("foo") 107 | .and(Person::getLastName) 108 | .like("web%") 109 | .and(Person::getFavouriteNumber) 110 | .equalTo(5) 111 | .select(personMapper, () -> mockConnection); 112 | 113 | assertEquals("fname", result.get().getFirstName()); 114 | assertEquals("lname", result.get().getLastName()); 115 | assertEquals((Integer)9001, result.get().getFavouriteNumber()); 116 | } 117 | 118 | @Test public void should_map_results_list() throws SQLException { 119 | when(mockConnection.prepareStatement(any(String.class))).thenReturn(mockStatement); 120 | when(mockStatement.executeQuery()).thenReturn(mockResults); 121 | when(mockResults.next()).thenReturn(true).thenReturn(false); 122 | when(mockResults.getObject("first_name")).thenReturn("fname"); 123 | when(mockResults.getObject("last_name")).thenReturn("lname"); 124 | when(mockResults.getObject("favourite_number")).thenReturn(9001); 125 | 126 | List result = from(Person.class) 127 | .where(Person::getFirstName) 128 | .equalTo("benji") 129 | .and(Person::getLastName) 130 | .notEqualTo("foo") 131 | .and(Person::getLastName) 132 | .like("web%") 133 | .and(Person::getFavouriteNumber) 134 | .equalTo(5) 135 | .list(personMapper, () -> mockConnection); 136 | 137 | assertEquals(1, result.size()); 138 | assertEquals("fname", result.get(0).getFirstName()); 139 | assertEquals("lname", result.get(0).getLastName()); 140 | assertEquals((Integer)9001, result.get(0).getFavouriteNumber()); 141 | } 142 | 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/test/java/uk/co/benjiweber/benjiql/update/DeleteTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.update; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.Mock; 6 | import org.mockito.junit.MockitoJUnitRunner; 7 | import uk.co.benjiweber.benjiql.example.Conspiracy; 8 | import uk.co.benjiweber.benjiql.example.Person; 9 | 10 | import java.sql.Connection; 11 | import java.sql.PreparedStatement; 12 | import java.sql.SQLException; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | import static org.mockito.ArgumentMatchers.any; 16 | import static org.mockito.Mockito.verify; 17 | import static org.mockito.Mockito.when; 18 | import static uk.co.benjiweber.benjiql.ddl.Create.relationship; 19 | import static uk.co.benjiweber.benjiql.update.Delete.delete; 20 | 21 | @RunWith(MockitoJUnitRunner.class) 22 | public class DeleteTest { 23 | 24 | @Mock Connection mockConnection; 25 | @Mock PreparedStatement mockStatement; 26 | 27 | @Test public void should_match_example_with_no_restrictions() { 28 | String sql = delete(Person.class).toSql(); 29 | assertEquals("DELETE FROM person", sql.trim()); 30 | } 31 | 32 | @Test public void should_match_example_with_restrictions() { 33 | String sql = delete(Person.class) 34 | .where(Person::getLastName) 35 | .equalTo("weber") 36 | .and(Person::getFavouriteNumber) 37 | .equalTo(6) 38 | .and(Person::getFirstName) 39 | .like("%w%") 40 | .and(Person::getFirstName) 41 | .notEqualTo("bob") 42 | .toSql(); 43 | 44 | assertEquals("DELETE FROM person WHERE last_name = ? AND favourite_number = ? AND first_name LIKE ? AND first_name != ?", sql.trim()); 45 | } 46 | 47 | @Test public void join_example() { 48 | String sql = delete(relationship(Conspiracy.class, Person.class)) 49 | .whereLeft(Conspiracy::getName) 50 | .equalTo("nsa") 51 | .andRight(Person::getLastName) 52 | .equalTo("smith") 53 | .toSql(); 54 | 55 | assertEquals("DELETE FROM conspiracy_person WHERE conspiracy_name = ? AND person_last_name = ?", sql.trim()); 56 | } 57 | 58 | @Test public void should_set_values() throws SQLException { 59 | when(mockConnection.prepareStatement(any(String.class))).thenReturn(mockStatement); 60 | 61 | delete(Person.class) 62 | .where(Person::getLastName) 63 | .equalTo("weber") 64 | .and(Person::getFavouriteNumber) 65 | .equalTo(6) 66 | .execute(() -> mockConnection); 67 | 68 | verify(mockStatement).setString(1,"weber"); 69 | verify(mockStatement).setInt(2,6); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/uk/co/benjiweber/benjiql/update/InsertTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.update; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.Mock; 6 | import org.mockito.junit.MockitoJUnitRunner; 7 | import uk.co.benjiweber.benjiql.example.Conspiracy; 8 | import uk.co.benjiweber.benjiql.example.Person; 9 | 10 | import java.sql.Connection; 11 | import java.sql.PreparedStatement; 12 | import java.sql.SQLException; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | import static org.mockito.ArgumentMatchers.any; 16 | import static org.mockito.Mockito.verify; 17 | import static org.mockito.Mockito.when; 18 | import static uk.co.benjiweber.benjiql.update.Upsert.insert; 19 | import static uk.co.benjiweber.benjiql.update.Upsert.update; 20 | 21 | @RunWith(MockitoJUnitRunner.class) 22 | public class InsertTest { 23 | 24 | Person person = new Person(); 25 | @Mock Connection mockConnection; 26 | @Mock PreparedStatement mockStatement; 27 | 28 | @Test public void should_match_example() { 29 | String sql = insert(person) 30 | .value(Person::getFirstName) 31 | .value(Person::getFavouriteNumber) 32 | .toSql(); 33 | 34 | assertEquals("INSERT INTO person (first_name, favourite_number) VALUES ( ?, ? )", sql.trim()); 35 | } 36 | 37 | @Test public void join_example() { 38 | Person smith = new Person("agent","smith"); 39 | Conspiracy nsa = new Conspiracy("nsa"); 40 | 41 | String sql = insert(nsa, smith) 42 | .valueLeft(Conspiracy::getName) 43 | .valueRight(Person::getLastName) 44 | .valueRight(Person::getFirstName) 45 | .toSql(); 46 | 47 | assertEquals("INSERT INTO conspiracy_person (conspiracy_name, person_last_name, person_first_name) VALUES ( ?, ?, ? )", sql.trim()); 48 | } 49 | 50 | @Test public void should_set_values() throws SQLException { 51 | when(mockConnection.prepareStatement(any(String.class))).thenReturn(mockStatement); 52 | 53 | person.setFirstName("asdf"); 54 | person.setFavouriteNumber(55); 55 | 56 | insert(person) 57 | .value(Person::getFirstName) 58 | .value(Person::getFavouriteNumber) 59 | .execute(() -> mockConnection); 60 | 61 | verify(mockStatement).setString(1, "asdf"); 62 | verify(mockStatement).setInt(2, 55); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/uk/co/benjiweber/benjiql/update/UpdateTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.benjiweber.benjiql.update; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.Mock; 6 | import org.mockito.junit.MockitoJUnitRunner; 7 | import uk.co.benjiweber.benjiql.example.Person; 8 | 9 | import java.sql.Connection; 10 | import java.sql.PreparedStatement; 11 | import java.sql.SQLException; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | import static org.mockito.ArgumentMatchers.any; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.when; 17 | import static uk.co.benjiweber.benjiql.update.Delete.delete; 18 | import static uk.co.benjiweber.benjiql.update.Upsert.update; 19 | 20 | @RunWith(MockitoJUnitRunner.class) 21 | public class UpdateTest { 22 | 23 | Person person = new Person(); 24 | @Mock Connection mockConnection; 25 | @Mock PreparedStatement mockStatement; 26 | 27 | @Test public void should_match_example_with_no_restrictions() { 28 | String sql = update(person) 29 | .value(Person::getFirstName) 30 | .value(Person::getFavouriteNumber) 31 | .toSql(); 32 | 33 | assertEquals("UPDATE person SET first_name = ?, favourite_number = ?", sql.trim()); 34 | } 35 | 36 | @Test public void should_match_example_with_restrictions() { 37 | String sql = update(person) 38 | .value(Person::getFirstName) 39 | .value(Person::getFavouriteNumber) 40 | .where(Person::getLastName) 41 | .equalTo("weber") 42 | .and(Person::getFirstName) 43 | .notEqualTo("bob") 44 | .and(Person::getFirstName) 45 | .like("b%") 46 | .toSql(); 47 | 48 | assertEquals("UPDATE person SET first_name = ?, favourite_number = ? WHERE last_name = ? AND first_name != ? AND first_name LIKE ?", sql.trim()); 49 | } 50 | 51 | @Test public void should_set_values() throws SQLException { 52 | when(mockConnection.prepareStatement(any(String.class))).thenReturn(mockStatement); 53 | 54 | person.setFirstName("asdf"); 55 | person.setFavouriteNumber(55); 56 | 57 | update(person) 58 | .value(Person::getFirstName) 59 | .value(Person::getFavouriteNumber) 60 | .where(Person::getLastName) 61 | .equalTo("weber") 62 | .and(Person::getFavouriteNumber) 63 | .equalTo(6) 64 | .execute(() -> mockConnection); 65 | 66 | verify(mockStatement).setString(1, "asdf"); 67 | verify(mockStatement).setInt(2, 55); 68 | verify(mockStatement).setString(3, "weber"); 69 | verify(mockStatement).setInt(4, 6); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /todo: -------------------------------------------------------------------------------- 1 | * Delete where 2 | * And/Or/Brackets 3 | * Select from join 4 | * Using 5 | * Asymmetric joins --------------------------------------------------------------------------------