From 6138a5ce200d0c1ebc2730fd565bc7bcfdcdc230 Mon Sep 17 00:00:00 2001 From: Clemens Fries Date: Thu, 19 May 2016 00:28:53 +0200 Subject: Fix alternative name handling for columns, plus a lot of documentation. --- src/main/java/de/xenoworld/ormpaloompa/Table.java | 126 +++++++++++++-------- .../java/de/xenoworld/ormpaloompa/WhereQuery.java | 29 ++++- .../xenoworld/ormpaloompa/annotations/Column.java | 20 ++++ .../xenoworld/ormpaloompa/annotations/Field.java | 16 --- .../ormpaloompa/annotations/TableInfo.java | 2 +- src/main/java/overview.adoc | 36 ++++-- .../java/de/xenoworld/ormpaloompa/TableTest.java | 43 ++++--- .../de/xenoworld/ormpaloompa/testutils/DBRule.java | 5 +- 8 files changed, 187 insertions(+), 90 deletions(-) create mode 100644 src/main/java/de/xenoworld/ormpaloompa/annotations/Column.java delete mode 100644 src/main/java/de/xenoworld/ormpaloompa/annotations/Field.java (limited to 'src') diff --git a/src/main/java/de/xenoworld/ormpaloompa/Table.java b/src/main/java/de/xenoworld/ormpaloompa/Table.java index 96497b6..6d23fcd 100644 --- a/src/main/java/de/xenoworld/ormpaloompa/Table.java +++ b/src/main/java/de/xenoworld/ormpaloompa/Table.java @@ -1,9 +1,10 @@ package de.xenoworld.ormpaloompa; -import de.xenoworld.ormpaloompa.annotations.Field; +import de.xenoworld.ormpaloompa.annotations.Column; import de.xenoworld.ormpaloompa.annotations.TableInfo; import org.apache.commons.lang3.tuple.Pair; +import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -16,10 +17,9 @@ import java.util.stream.Stream; // TODO: Filter out fields where the value is null // TODO: Filter out the identity fields when updating -// (insert identities only when they are not null → auto IDs, but things like the term-database do -// have a string as key) +// (insert identities only when they are not null → auto IDs, but things +// like the term-database do have a string as key) // TODO: Use more Optionals and get rid of exception throwing where possible -// Name: Ormpaloompa /** * A SQLite table of a POJO `T` as a proxy. * @@ -31,10 +31,10 @@ import java.util.stream.Stream; * [source,java] * ---- * class Bird { - * @Field(identity = true) + * @Column(identity = true) * public Integer id; * - * @Field + * @Column * public String name; * } * ---- @@ -57,9 +57,12 @@ public class Table { private final Connection connection; private final String tableName; - private final TableField identity; - private ArrayList tableFields; + private final TableColumn identity; + private ArrayList tableColumns; + /** + * A central location for all query strings. + */ private enum Query { COUNT("SELECT COUNT(*) FROM %s"), GET_BY_ID("SELECT * FROM %s WHERE %s = ?"), @@ -79,23 +82,33 @@ public class Table { } } - - private class TableField { + /** + * Describes a column in the database. + * + * Properties are: + * `field`:: + * The annotated field. + * optional `name`:: + * Can be used to specify an alternative name for a column. + * optional `identity`:: + * Used to mark the primary key. If omitted, the column with the name + * `Id` is used. + */ + private class TableColumn { final String name; - final String dflt; + final Field field; final boolean identity; - private TableField(String name, String dflt, boolean identity) { + private TableColumn(Field field, String name, boolean identity) { + this.field = field; this.name = name; - this.dflt = dflt; this.identity = identity; } @Override public String toString() { - return "TableField{" + + return "TableColumn{" + "name='" + name + '\'' + - ", dflt='" + dflt + '\'' + ", identity=" + identity + '}'; } @@ -105,14 +118,14 @@ public class Table { this.tableClass = tableClass; this.connection = connection; - tableFields = new ArrayList<>(); + tableColumns = new ArrayList<>(); Stream.of(tableClass.getFields()) - .map(field -> Pair.of(field.getName(), field.getAnnotation(Field.class))) - .forEach(p -> tableFields.add( - new TableField( - p.getRight().name().isEmpty() ? p.getLeft() : p.getRight().name(), - p.getRight().dflt(), + .map(field -> Pair.of(field, field.getAnnotation(Column.class))) + .forEach(p -> tableColumns.add( + new TableColumn( + p.getLeft(), + p.getRight().name().isEmpty() ? p.getLeft().getName() : p.getRight().name(), p.getRight().identity()))); this.tableName = discoverTableName(tableClass); @@ -120,12 +133,12 @@ public class Table { } // TODO: This is kind of an idiotic contraption - private TableField discoverIdentity(Class tableClass) { - return tableFields.stream() - .filter(f -> f.identity) + private TableColumn discoverIdentity(Class tableClass) { + return tableColumns.stream() + .filter(c -> c.identity) .findFirst() - .orElseGet(() -> tableFields.stream() - .filter(f -> f.name.toLowerCase().equals("id")) + .orElseGet(() -> tableColumns.stream() + .filter(c -> c.name.toLowerCase().equals("id")) .findFirst() .orElseThrow(() -> new RuntimeException( String.format("%s: No identity given, and no field 'id' present.", @@ -139,8 +152,8 @@ public class Table { * Get name for table from TableInfo annotation or append "s" to the lower * case table name. * - * @param tableClass table to inspect - * @return name of table in database + * @param tableClass table to inspect. + * @return name of table in database. */ private String discoverTableName(Class tableClass) { TableInfo tableInfo = tableClass.getAnnotation(TableInfo.class); @@ -154,7 +167,7 @@ public class Table { /** * Return the number of rows in the table, uses `COUNT(*)`. * - * @return total number of rows + * @return total number of rows. * @throws SQLException */ public Integer count() throws SQLException { @@ -168,7 +181,7 @@ public class Table { /** * Get a `T` by Identity. * - * @param id identity + * @param id identity. * @return `Optional`, `empty()` if no object could be found, or if an * error occurred. */ @@ -208,9 +221,9 @@ public class Table { * * This will create a query for `… WHERE foo = bar …`. * - * @param whereQuery a query - * @param args (optional) arguments to WHERE clause - * @return a Stream of matching `T` + * @param whereQuery a query. + * @param args (optional) arguments to WHERE clause. + * @return a Stream of matching `T`. */ public Stream find(WhereQuery whereQuery, Object... args) { // TODO: Make try-mess better @@ -236,28 +249,44 @@ public class Table { } } + /** + * Create a new `T` from a ResultSet. + * + * @param result ResultSet to construct `T` from. + * @return a new `T`. + * @throws InstantiationException + * @throws IllegalAccessException + */ private T fromResultSet(ResultSet result) throws InstantiationException, IllegalAccessException { T t = tableClass.newInstance(); - tableFields.forEach(f -> { + tableColumns.forEach(c -> { try { - t.getClass().getField(f.name).set(t, result.getObject(f.name)); + t.getClass().getField(c.field.getName()).set(t, result.getObject(c.name)); } catch (IllegalAccessException | NoSuchFieldException | SQLException e) { e.printStackTrace(); } }); + return t; } + /** + * Insert a `T` into the database. + * + * @param t `T` to insert. + * @return an insert id. + * @throws SQLException + */ public Long insert(T t) throws SQLException { - String fields = tableFields + String fields = tableColumns .stream() .map(f -> f.name) .collect(Collectors.joining(",")); StringBuffer placeholder = new StringBuffer("?"); - IntStream.range(0, tableFields.size() - 1).forEach(i -> placeholder.append(", ?")); + IntStream.range(0, tableColumns.size() - 1).forEach(i -> placeholder.append(", ?")); String query = String.format(Query.INSERT.toString(), tableName, fields, placeholder); PreparedStatement stmt = connection.prepareStatement(query); @@ -269,8 +298,13 @@ public class Table { return stmt.getGeneratedKeys().getLong(1); } + /** + * Update an existing `T`. + * + * @param t `T` to update. + */ public void update(T t) { - String fields = tableFields + String fields = tableColumns .stream() .map(f -> f.name) .collect(Collectors.joining(" = ?, ")); @@ -290,9 +324,9 @@ public class Table { private int fillStatementFromObject(T t, PreparedStatement stmt) { final int[] order = {1}; - tableFields.stream().forEach(f -> { + tableColumns.stream().forEach(c -> { try { - Object o = t.getClass().getField(f.name).get(t); + Object o = t.getClass().getField(c.field.getName()).get(t); stmt.setObject(order[0]++, o); } catch (IllegalAccessException | NoSuchFieldException | SQLException e) { e.printStackTrace(); @@ -303,10 +337,10 @@ public class Table { } /** - * Take `t` and insert a new entry, or update an existing entry. + * Insert `T`, or update if it already exists. * - * @param t - * @return Optional of insert Id (a Long) + * @param t `T` to insert or update. + * @return a `Optional<>` of insert id (a `Long`). * @throws NoSuchFieldException * @throws IllegalAccessException * @throws SQLException @@ -328,13 +362,13 @@ public class Table { } /** - * Retrieve the current value of the `@Identity` field. + * Retrieve the current value of the `@Column(identity = true)` field. * * Will return an empty Optional if the value is null, or if there was * an Exception. * - * @param t object to retrieve identity value from - * @return value of identity, or empty on null or error + * @param t object to retrieve identity value from. + * @return value of identity, or empty on null or error. */ public Optional retrieveIdValue(T t) { try { diff --git a/src/main/java/de/xenoworld/ormpaloompa/WhereQuery.java b/src/main/java/de/xenoworld/ormpaloompa/WhereQuery.java index fcc42eb..4047be4 100644 --- a/src/main/java/de/xenoworld/ormpaloompa/WhereQuery.java +++ b/src/main/java/de/xenoworld/ormpaloompa/WhereQuery.java @@ -3,7 +3,9 @@ package de.xenoworld.ormpaloompa; import java.util.Optional; public class WhereQuery { + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private Optional limit = Optional.empty(); + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private Optional orderFields = Optional.empty(); private Order order; private String where; @@ -34,17 +36,40 @@ public class WhereQuery { this.where = where; } + /** + * Set `LIMIT` on `WHERE` clause. + * + * @param limit maximum number of results to return. + * @return same WhereQuery. + */ public WhereQuery limit(Integer limit) { this.limit = Optional.of(limit); return this; } - public WhereQuery orderBy(String fields, Order order) { - orderFields = Optional.of(fields); + /** + * Set `ORDER BY` on `WHERE` clause. + * + * `columns` may be one column name, or multiple names separated by comma. + * + * @param columns one or more columns to `ORDER BY` + * @param order ascending or descending order. + * @return same WhereQuery. + */ + public WhereQuery orderBy(String columns, Order order) { + orderFields = Optional.of(columns); this.order = order; return this; } + /** + * Add the `WHERE` query. + * + * This is something like `foo = 1234` or `bar = ?`. + * + * @param where the actual where query. + * @return same WhereQuery. + */ public static WhereQuery where(String where) { return new WhereQuery(where); } diff --git a/src/main/java/de/xenoworld/ormpaloompa/annotations/Column.java b/src/main/java/de/xenoworld/ormpaloompa/annotations/Column.java new file mode 100644 index 0000000..92a917b --- /dev/null +++ b/src/main/java/de/xenoworld/ormpaloompa/annotations/Column.java @@ -0,0 +1,20 @@ +package de.xenoworld.ormpaloompa.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotate a public field. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Column { + /** + * If non-empty, the column name to be used. + */ + String name() default ""; + + /** + * `true` if the field is the table's identity. + */ + boolean identity() default false; +} \ No newline at end of file diff --git a/src/main/java/de/xenoworld/ormpaloompa/annotations/Field.java b/src/main/java/de/xenoworld/ormpaloompa/annotations/Field.java deleted file mode 100644 index 5cd917b..0000000 --- a/src/main/java/de/xenoworld/ormpaloompa/annotations/Field.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.xenoworld.ormpaloompa.annotations; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Annotate a public field. - */ -@Retention(RetentionPolicy.RUNTIME) -public @interface Field { - String name() default ""; - - String dflt() default ""; - - boolean identity() default false; -} \ No newline at end of file diff --git a/src/main/java/de/xenoworld/ormpaloompa/annotations/TableInfo.java b/src/main/java/de/xenoworld/ormpaloompa/annotations/TableInfo.java index 5094c5d..04c2d23 100644 --- a/src/main/java/de/xenoworld/ormpaloompa/annotations/TableInfo.java +++ b/src/main/java/de/xenoworld/ormpaloompa/annotations/TableInfo.java @@ -4,7 +4,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Optional table annotation, can be used to set the table name in the database + * Optional table annotation, can be used to set the table name in the database. */ @Retention(RetentionPolicy.RUNTIME) public @interface TableInfo { diff --git a/src/main/java/overview.adoc b/src/main/java/overview.adoc index 90a82f6..90642bb 100644 --- a/src/main/java/overview.adoc +++ b/src/main/java/overview.adoc @@ -1,18 +1,32 @@ -= Eduard XVII — The loyal MUC servant +== Ormpaloompa — Sort-of-ORM for EduardXVII -== Continuous Integration +Because the friendly MUC chat bot EduardXVII needs to deal in several places +with simple-structured SQLite tables, I created this tiny object-relational +mapper. It stinks in several places, but it also gets its job done. Coverage is +over 85% and should go further north. -We currently use the CI of Gitlab.com, but it might be warranted to use a -dedicated runner which could execute the requests much faster. This is work -for Future Homer. +It mainly consists of the class `Table` which can be used as follows. -Currently everything is configured in the file `.gitlab-ci.yml`: +As simple class could look like this: -[source,yaml] +[source,java] ---- -include::../../../.gitlab-ci.yml[] +class Bird { + @Column(identity = true) + public Integer id; + + @Column + public String name; +} ---- -NOTE: Replacing `ec.SunEC` is done here because there were problems in the - Docker container with OpenJDK. Apparently making a HTTPS connection - to the Gradle repositories would not work. \ No newline at end of file +Then create a `Table` object and use it to interact with the database. + +[source,java] +---- +Table t = new Table<>(Bird.class, someDatabaseConnection); +Bird newBird = new Bird(); +newBird.name = "A new bird"; +Object id = table.insert(newBird); +Bird storedBird = t.getById(id).get(); +---- diff --git a/src/test/java/de/xenoworld/ormpaloompa/TableTest.java b/src/test/java/de/xenoworld/ormpaloompa/TableTest.java index 79b3a0c..d73b45f 100644 --- a/src/test/java/de/xenoworld/ormpaloompa/TableTest.java +++ b/src/test/java/de/xenoworld/ormpaloompa/TableTest.java @@ -1,6 +1,6 @@ package de.xenoworld.ormpaloompa; -import de.xenoworld.ormpaloompa.annotations.Field; +import de.xenoworld.ormpaloompa.annotations.Column; import de.xenoworld.ormpaloompa.annotations.TableInfo; import de.xenoworld.ormpaloompa.testutils.DBRule; import org.junit.Ignore; @@ -18,27 +18,27 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; /** - * Test object with explicit identity + * Test object with explicit identity, and an alternative name for a field. */ class Bird { - @Field(identity = true) + @Column(identity = true) public Integer id; - @Field + @Column public String name; - @Field + @Column(name = "descr") public String description; } /** - * Test object with no explicit identity + * Test object with no explicit identity. */ class Rabbit { - @Field + @Column public Integer id; - @Field + @Column public String name; } @@ -48,10 +48,10 @@ class Rabbit { */ @TableInfo(tableName = "foxes") class Fox { - @Field(identity = true) + @Column(identity = true) public String name; - @Field(identity = true) + @Column(identity = true) public Integer id; } @@ -60,13 +60,13 @@ class Fox { */ @TableInfo class Monkey { - @Field + @Column public String name; } public class TableTest { static String[] FIXTURES = { - "CREATE TABLE birds (id INTEGER PRIMARY KEY, name TEXT, description TEXT);", + "CREATE TABLE birds (id INTEGER PRIMARY KEY, name TEXT, descr TEXT);", "INSERT INTO birds " + "(id, name) " + "VALUES " + @@ -100,7 +100,7 @@ public class TableTest { public DBRule dbRule = new DBRule(FIXTURES); /** - * The field `id` should be annotated as `@Field`, but it should not have + * The field `id` should be annotated as `@Column`, but it should not have * property `identity` set to `true`. The field should be recognised as * identity by virtue of being named `id`. */ @@ -239,4 +239,21 @@ public class TableTest { assertThat("Table size did not change", table.count(), is(4)); assertThat("No new entries were added", insertId.isPresent(), is(false)); } + + @Test + public void testFind_brokenQuery() throws Exception { + long count; + Table table = new Table<>(Bird.class, dbRule.getConnection()); + + Bird newBird = new Bird(); + newBird.name = "A new bird"; + + table.insert(newBird); + + count = table.find(where("id = 1")).count(); + assertThat("Table has one entry", count, is(1L)); + + count = table.find(where("broken = 1234")).count(); + assertThat("Query returns turns up empty", count, is(0L)); + } } \ No newline at end of file diff --git a/src/test/java/de/xenoworld/ormpaloompa/testutils/DBRule.java b/src/test/java/de/xenoworld/ormpaloompa/testutils/DBRule.java index 0bb12f7..39da3d8 100644 --- a/src/test/java/de/xenoworld/ormpaloompa/testutils/DBRule.java +++ b/src/test/java/de/xenoworld/ormpaloompa/testutils/DBRule.java @@ -2,7 +2,10 @@ package de.xenoworld.ormpaloompa.testutils; import org.junit.rules.ExternalResource; -import java.sql.*; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; import java.util.stream.Stream; public class DBRule extends ExternalResource { -- cgit