diff --git a/.classpath b/.classpath deleted file mode 100644 index 4559ca0..0000000 --- a/.classpath +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.gitignore b/.gitignore index bdc8dcd..fd524ad 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,11 @@ /etc/ /h2test.mv.db /sqlitetest.db -/.settings/ +bin/ +tmp/ +.metadata +.classpath +.settings +.project .idea /.DS_Store diff --git a/.project b/.project deleted file mode 100644 index 44345cc..0000000 --- a/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - norm - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.m2e.core.maven2Nature - - diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 4c28b1a..0000000 --- a/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding//src/test/java=UTF-8 -encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index ad8a1d0..0000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,8 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore -org.eclipse.jdt.core.compiler.release=enabled -org.eclipse.jdt.core.compiler.source=11 diff --git a/LICENSE.md b/LICENSE.md index afb0a3a..5625ffc 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,13 +1,13 @@ -Copyright 2014, Dieselpoint, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and +Copyright 2014, Dieselpoint, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/pom.xml b/pom.xml index b179647..17543a3 100644 --- a/pom.xml +++ b/pom.xml @@ -1,213 +1,217 @@ - - - 4.0.0 - - com.dieselpoint - norm - Norm - 1.0.7 - jar - - An extremely lightweight data access layer over JDBC - https://github.com/dieselpoint/norm - - - Dieselpoint, Inc. - https://github.com/dieselpoint - - - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - https://github.com/dieselpoint/norm - scm:git:https://github.com/dieselpoint/norm.git - - - - - Chris Cleveland - https://github.com/dieselpoint - - - - - UTF-8 - true - - - - - - - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.1.0 - - - enforce-maven - - enforce - - - - - 3.2.5 - - - - - - - - maven-compiler-plugin - 3.10.1 - - 11 - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.4.1 - - ${java.home}/bin/javadoc - -Xdoclint:none - -Xdoclint:none - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.0.1 - - - sign-artifacts - deploy - - sign - - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 - true - - ossrh - https://oss.sonatype.org/ - true - - - - - - - - - - javax.persistence - javax.persistence-api - 2.2 - - - - com.zaxxer - HikariCP - 5.0.1 - - - - mysql - mysql-connector-java - 8.0.30 - test - - - - org.postgresql - postgresql - 42.5.1 - test - - - - com.h2database - h2 - 2.2.220 - test - - - - org.xerial - sqlite-jdbc - 3.41.2.2 - test - - - - org.apache.derby - derby - 10.16.1.1 - test - - - - org.mockito - mockito-all - 1.10.19 - test - - - - junit - junit - 4.13.2 - test - - - - + + + 4.0.0 + + com.dieselpoint + norm + Norm + 1.0.7 + jar + + An extremely lightweight data access layer over JDBC + https://github.com/dieselpoint/norm + + + Dieselpoint, Inc. + https://github.com/dieselpoint + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + https://github.com/dieselpoint/norm + scm:git:https://github.com/dieselpoint/norm.git + + + + + Chris Cleveland + https://github.com/dieselpoint + + + + + UTF-8 + true + + + + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.1.0 + + + enforce-maven + + enforce + + + + + 3.2.5 + + + + + + + + maven-compiler-plugin + 3.10.1 + + 11 + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.1 + + ${java.home}/bin/javadoc + -Xdoclint:none + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + deploy + + sign + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.13 + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + + + + javax.persistence + javax.persistence-api + 2.2 + + + + com.zaxxer + HikariCP + 5.0.1 + + + + mysql + mysql-connector-java + 8.0.30 + test + + + + org.postgresql + postgresql + 42.5.1 + test + + + + com.h2database + h2 + 2.2.220 + test + + + + org.xerial + sqlite-jdbc + 3.41.2.2 + test + + + + org.apache.derby + derby + 10.16.1.1 + test + + + + org.mockito + mockito-all + 1.10.19 + test + + + + junit + junit + 4.13.2 + test + + + + \ No newline at end of file diff --git a/src/main/java/com/dieselpoint/norm/ColumnOrder.java b/src/main/java/com/dieselpoint/norm/ColumnOrder.java index 7ec61f0..4fc9c61 100644 --- a/src/main/java/com/dieselpoint/norm/ColumnOrder.java +++ b/src/main/java/com/dieselpoint/norm/ColumnOrder.java @@ -9,7 +9,7 @@ /** * Specify the order of the columns. Is used in the create table sql. * ColumnOrder({"name","address", ...}) - * + * * @author ccleve */ @Target({ElementType.TYPE}) @@ -17,5 +17,4 @@ @Documented public @interface ColumnOrder { String [] value(); -} - +} \ No newline at end of file diff --git a/src/main/java/com/dieselpoint/norm/Database.java b/src/main/java/com/dieselpoint/norm/Database.java index eb0f74e..d7352e1 100644 --- a/src/main/java/com/dieselpoint/norm/Database.java +++ b/src/main/java/com/dieselpoint/norm/Database.java @@ -1,315 +1,315 @@ -package com.dieselpoint.norm; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.sql.DataSource; - -import com.dieselpoint.norm.latency.DbLatencyWarning; -import com.dieselpoint.norm.latency.LatencyAlerter; -import com.dieselpoint.norm.sqlmakers.SqlMaker; -import com.dieselpoint.norm.sqlmakers.StandardSqlMaker; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; - -/** - * Provides methods to access a database. - */ -public class Database { - - protected SqlMaker sqlMaker = new StandardSqlMaker(); - protected DataSource ds; - - protected String dataSourceClassName = System.getProperty("norm.dataSourceClassName"); - protected String driverClassName = System.getProperty("norm.driverClassName"); - protected String jdbcUrl = System.getProperty("norm.jdbcUrl"); - protected String serverName = System.getProperty("norm.serverName"); - protected String databaseName = System.getProperty("norm.databaseName"); - protected String user = System.getProperty("norm.user"); - protected String password = System.getProperty("norm.password"); - protected int maxPoolSize = 10; - protected long maxLatency = System.getProperty("norm.maxLatency") != null ? Integer.parseInt( System.getProperty("norm.maxLatency") ) : -1; - protected ArrayList latencyAlerters = new ArrayList<>(); - - protected Map dataSourceProperties = new HashMap<>(); - - /** - * Set the maker object for the particular flavor of sql. - */ - public void setSqlMaker(SqlMaker sqlMaker) { - this.sqlMaker = sqlMaker; - } - - public SqlMaker getSqlMaker() { - return sqlMaker; - } - - /** - * Provides the DataSource used by this database. Override this method to change - * how the DataSource is created or configured. - */ - protected DataSource getDataSource() throws SQLException { - HikariConfig config = new HikariConfig(); - config.setMaximumPoolSize(maxPoolSize); - - if (dataSourceClassName != null) { - config.setDataSourceClassName(dataSourceClassName); - } - - if (driverClassName != null) { - config.setDriverClassName(driverClassName); - } - - if (jdbcUrl != null) { - config.setJdbcUrl(jdbcUrl); - } - - addDataSourceProperty("serverName", serverName); - addDataSourceProperty("databaseName", databaseName); - addDataSourceProperty("user", user); - addDataSourceProperty("password", password); - - for (Map.Entry entry : dataSourceProperties.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - if (value != null) { - config.addDataSourceProperty(key, value); - } - } - - /* - * addConfigProperty(config, "serverName", serverName); - * addConfigProperty(config, "databaseName", databaseName); - * addConfigProperty(config, "user", user); addConfigProperty(config, - * "password", password); - */ - - config.setLeakDetectionThreshold(30000); - - return new HikariDataSource(config); - } - - public void addDataSourceProperty(String name, String value) { - dataSourceProperties.put(name, value); - } - - /* - * private void addConfigProperty(HikariConfig config, String name, String - * value) { if (value != null) { config.addDataSourceProperty(name, value); } } - */ - - /** - * Create a query using straight SQL. Overrides any other methods like .where(), - * .orderBy(), etc. - * - * @param sql The SQL string to use, may include ? parameters. - * @param args The parameter values to use in the query. - */ - public Query sql(String sql, Object... args) { - return new Query(this).sql(sql, args); - } - - /** - * Create a query with the given where clause. - * - * @param where Example: "name=?" - * @param args The parameter values to use in the where, example: "Bob" - */ - public Query where(String where, Object... args) { - return new Query(this).where(where, args); - } - - /** - * Create a query with the given "order by" clause. - */ - public Query orderBy(String orderBy) { - return new Query(this).orderBy(orderBy); - } - - /** - * Returns a JDBC connection. Can be useful if you need to customize how - * transactions work, but you shouldn't normally need to call this method. You - * must close the connection after you're done with it. - */ - public Connection getConnection() { - try { - - if (ds == null) { - ds = getDataSource(); - } - return ds.getConnection(); - - } catch (Throwable t) { - throw new DbException(t); - } - } - - /** - * Simple, primitive method for creating a table based on a pojo. Does not add - * indexes or implement complex data types. Probably not suitable for production - * use. - */ - public Query createTable(Class clazz) { - return new Query(this).createTable(clazz); - } - - /** - * Insert a row into a table. The row pojo can have a @Table annotation to - * specify the table, or you can specify the table with the .table() method. - */ - public Query insert(Object row) { - return new Query(this).insert(row); - } - - /** - * See {@link com.dieselpoint.norm.Query#generatedKeyReceiver(Object, String...) - * generateKeyReceiver} method. - */ - public Query generatedKeyReceiver(Object generatedKeyReceiver, String... generatedKeyNames) { - return new Query(this).generatedKeyReceiver(generatedKeyReceiver, generatedKeyNames); - } - - /** - * Delete a row in a table. This method looks for an @Id annotation to find the - * row to delete by primary key, and looks for a @Table annotation to figure out - * which table to hit. - */ - public Query delete(Object row) { - return new Query(this).delete(row); - } - - /** - * Execute a "select" query and get some results. The system will create a new - * object of type "clazz" for each row in the result set and add it to a List. - * It will also try to extract the table name from a @Table annotation in the - * clazz. - */ - public List results(Class clazz) { - return new Query(this).results(clazz); - } - - /** - * Returns the first row in a query in a pojo. Will return it in a Map if a - * class that implements Map is specified. - */ - public T first(Class clazz) { - return new Query(this).first(clazz); - } - - /** - * Update a row in a table. It will match an existing row based on the primary - * key. - */ - public Query update(Object row) { - return new Query(this).update(row); - } - - /** - * Upsert a row in a table. It will insert, and if that fails, do an update with - * a match on a primary key. - */ - public Query upsert(Object row) { - return new Query(this).upsert(row); - } - - /** - * Create a query and specify which table it operates on. - */ - public Query table(String table) { - return new Query(this).table(table); - } - - /** - * Start a database transaction. Pass the transaction object to each query or - * command that should be part of the transaction using the .transaction() - * method. Then call transaction.commit() or .rollback() to complete the - * process. No need to close the transaction. - * - * @return a transaction object - */ - public Transaction startTransaction() { - return new Transaction( this, getConnection() ); - } - - /** - * Create a query that uses this transaction object. - */ - public Query transaction(Transaction trans) { - return new Query(this).transaction(trans); - } - - public void close() { - if (ds instanceof HikariDataSource) { - ((HikariDataSource) ds).close(); - } - } - - public void setDataSourceClassName(String dataSourceClassName) { - this.dataSourceClassName = dataSourceClassName; - } - - public void setDriverClassName(String driverClassName) { - this.driverClassName = driverClassName; - } - - public void setJdbcUrl(String jdbcUrl) { - this.jdbcUrl = jdbcUrl; - } - - public void setServerName(String serverName) { - this.serverName = serverName; - } - - public void setDatabaseName(String databaseName) { - this.databaseName = databaseName; - } - - public void setUser(String user) { - this.user = user; - } - - public void setPassword(String password) { - this.password = password; - } - - public int getMaxPoolSize() { - return maxPoolSize; - } - - public void setMaxPoolSize(int maxPoolSize) { - this.maxPoolSize = maxPoolSize; - } - - public long getMaxLatencyMillis() { return maxLatency; } - - /** - * @param millis the maximum latency that all {@link Query} or {@link Transaction#commit()} calls should tolerate. - * By default millis is set to {@code -1 } which turns off all latency alerting. Note that setting maxLatency to {@code 0} is - * an easy way to log all SQL Statements. This value can also be set using environment variable {@code norm.maxLatency } - */ - public void setMaxLatency( long millis ) { - this.maxLatency = millis; - } - - /** - * Adds the provided {@link LatencyAlerter} instance to the instances that are called in-order, when a - * {@link Query} or - * {@link Transaction#commit()} call to the database exceeds the maximum latency (either the global maximum set via - * {@link #setMaxLatency(long)}, or {@link Query#maxLatency(long)} or - * {@link Transaction#maxLatency(long)} - * @param alerter, the alerter to add - */ - public void addLatencyAlerter( LatencyAlerter alerter ) { - this.latencyAlerters.add( alerter ); - } - - public void alertLatency( DbLatencyWarning latencyWarning ) { - for (LatencyAlerter a : latencyAlerters) { - a.alertLatencyFailure( latencyWarning ); - } - } -} +package com.dieselpoint.norm; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import com.dieselpoint.norm.latency.DbLatencyWarning; +import com.dieselpoint.norm.latency.LatencyAlerter; +import com.dieselpoint.norm.sqlmakers.SqlMaker; +import com.dieselpoint.norm.sqlmakers.StandardSqlMaker; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +/** + * Provides methods to access a database. + */ +public class Database { + + protected SqlMaker sqlMaker = new StandardSqlMaker(); + protected DataSource ds; + + protected String dataSourceClassName = System.getProperty("norm.dataSourceClassName"); + protected String driverClassName = System.getProperty("norm.driverClassName"); + protected String jdbcUrl = System.getProperty("norm.jdbcUrl"); + protected String serverName = System.getProperty("norm.serverName"); + protected String databaseName = System.getProperty("norm.databaseName"); + protected String user = System.getProperty("norm.user"); + protected String password = System.getProperty("norm.password"); + protected int maxPoolSize = 10; + protected long maxLatency = System.getProperty("norm.maxLatency") != null ? Integer.parseInt( System.getProperty("norm.maxLatency") ) : -1; + protected ArrayList latencyAlerters = new ArrayList<>(); + + protected Map dataSourceProperties = new HashMap<>(); + + /** + * Set the maker object for the particular flavor of sql. + */ + public void setSqlMaker(SqlMaker sqlMaker) { + this.sqlMaker = sqlMaker; + } + + public SqlMaker getSqlMaker() { + return sqlMaker; + } + + /** + * Provides the DataSource used by this database. Override this method to change + * how the DataSource is created or configured. + */ + protected DataSource getDataSource() throws SQLException { + HikariConfig config = new HikariConfig(); + config.setMaximumPoolSize(maxPoolSize); + + if (dataSourceClassName != null) { + config.setDataSourceClassName(dataSourceClassName); + } + + if (driverClassName != null) { + config.setDriverClassName(driverClassName); + } + + if (jdbcUrl != null) { + config.setJdbcUrl(jdbcUrl); + } + + addDataSourceProperty("serverName", serverName); + addDataSourceProperty("databaseName", databaseName); + addDataSourceProperty("user", user); + addDataSourceProperty("password", password); + + for (Map.Entry entry : dataSourceProperties.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (value != null) { + config.addDataSourceProperty(key, value); + } + } + + /* + * addConfigProperty(config, "serverName", serverName); + * addConfigProperty(config, "databaseName", databaseName); + * addConfigProperty(config, "user", user); addConfigProperty(config, + * "password", password); + */ + + config.setLeakDetectionThreshold(30000); + + return new HikariDataSource(config); + } + + public void addDataSourceProperty(String name, String value) { + dataSourceProperties.put(name, value); + } + + /* + * private void addConfigProperty(HikariConfig config, String name, String + * value) { if (value != null) { config.addDataSourceProperty(name, value); } } + */ + + /** + * Create a query using straight SQL. Overrides any other methods like .where(), + * .orderBy(), etc. + * + * @param sql The SQL string to use, may include ? parameters. + * @param args The parameter values to use in the query. + */ + public Query sql(String sql, Object... args) { + return new Query(this).sql(sql, args); + } + + /** + * Create a query with the given where clause. + * + * @param where Example: "name=?" + * @param args The parameter values to use in the where, example: "Bob" + */ + public Query where(String where, Object... args) { + return new Query(this).where(where, args); + } + + /** + * Create a query with the given "order by" clause. + */ + public Query orderBy(String orderBy) { + return new Query(this).orderBy(orderBy); + } + + /** + * Returns a JDBC connection. Can be useful if you need to customize how + * transactions work, but you shouldn't normally need to call this method. You + * must close the connection after you're done with it. + */ + public Connection getConnection() { + try { + + if (ds == null) { + ds = getDataSource(); + } + return ds.getConnection(); + + } catch (Throwable t) { + throw new DbException(t); + } + } + + /** + * Simple, primitive method for creating a table based on a pojo. Does not add + * indexes or implement complex data types. Probably not suitable for production + * use. + */ + public Query createTable(Class clazz) { + return new Query(this).createTable(clazz); + } + + /** + * Insert a row into a table. The row pojo can have a @Table annotation to + * specify the table, or you can specify the table with the .table() method. + */ + public Query insert(Object row) { + return new Query(this).insert(row); + } + + /** + * See {@link com.dieselpoint.norm.Query#generatedKeyReceiver(Object, String...) + * generateKeyReceiver} method. + */ + public Query generatedKeyReceiver(Object generatedKeyReceiver, String... generatedKeyNames) { + return new Query(this).generatedKeyReceiver(generatedKeyReceiver, generatedKeyNames); + } + + /** + * Delete a row in a table. This method looks for an @Id annotation to find the + * row to delete by primary key, and looks for a @Table annotation to figure out + * which table to hit. + */ + public Query delete(Object row) { + return new Query(this).delete(row); + } + + /** + * Execute a "select" query and get some results. The system will create a new + * object of type "clazz" for each row in the result set and add it to a List. + * It will also try to extract the table name from a @Table annotation in the + * clazz. + */ + public List results(Class clazz) { + return new Query(this).results(clazz); + } + + /** + * Returns the first row in a query in a pojo. Will return it in a Map if a + * class that implements Map is specified. + */ + public T first(Class clazz) { + return new Query(this).first(clazz); + } + + /** + * Update a row in a table. It will match an existing row based on the primary + * key. + */ + public Query update(Object row) { + return new Query(this).update(row); + } + + /** + * Upsert a row in a table. It will insert, and if that fails, do an update with + * a match on a primary key. + */ + public Query upsert(Object row) { + return new Query(this).upsert(row); + } + + /** + * Create a query and specify which table it operates on. + */ + public Query table(String table) { + return new Query(this).table(table); + } + + /** + * Start a database transaction. Pass the transaction object to each query or + * command that should be part of the transaction using the .transaction() + * method. Then call transaction.commit() or .rollback() to complete the + * process. No need to close the transaction. + * + * @return a transaction object + */ + public Transaction startTransaction() { + return new Transaction( this, getConnection() ); + } + + /** + * Create a query that uses this transaction object. + */ + public Query transaction(Transaction trans) { + return new Query(this).transaction(trans); + } + + public void close() { + if (ds instanceof HikariDataSource) { + ((HikariDataSource) ds).close(); + } + } + + public void setDataSourceClassName(String dataSourceClassName) { + this.dataSourceClassName = dataSourceClassName; + } + + public void setDriverClassName(String driverClassName) { + this.driverClassName = driverClassName; + } + + public void setJdbcUrl(String jdbcUrl) { + this.jdbcUrl = jdbcUrl; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public void setDatabaseName(String databaseName) { + this.databaseName = databaseName; + } + + public void setUser(String user) { + this.user = user; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getMaxPoolSize() { + return maxPoolSize; + } + + public void setMaxPoolSize(int maxPoolSize) { + this.maxPoolSize = maxPoolSize; + } + + public long getMaxLatencyMillis() { return maxLatency; } + + /** + * @param millis the maximum latency that all {@link Query} or {@link Transaction#commit()} calls should tolerate. + * By default millis is set to {@code -1 } which turns off all latency alerting. Note that setting maxLatency to {@code 0} is + * an easy way to log all SQL Statements. This value can also be set using environment variable {@code norm.maxLatency } + */ + public void setMaxLatency( long millis ) { + maxLatency = millis; + } + + /** + * Adds the provided {@link LatencyAlerter} instance to the instances that are called in-order, when a + * {@link Query} or + * {@link Transaction#commit()} call to the database exceeds the maximum latency (either the global maximum set via + * {@link #setMaxLatency(long)}, or {@link Query#maxLatency(long)} or + * {@link Transaction#maxLatency(long)} + * @param alerter, the alerter to add + */ + public void addLatencyAlerter( LatencyAlerter alerter ) { + latencyAlerters.add( alerter ); + } + + public void alertLatency( DbLatencyWarning latencyWarning ) { + for (LatencyAlerter a : latencyAlerters) { + a.alertLatencyFailure( latencyWarning ); + } + } +} diff --git a/src/main/java/com/dieselpoint/norm/DbException.java b/src/main/java/com/dieselpoint/norm/DbException.java index 7b10e4b..a628afb 100644 --- a/src/main/java/com/dieselpoint/norm/DbException.java +++ b/src/main/java/com/dieselpoint/norm/DbException.java @@ -1,34 +1,34 @@ -package com.dieselpoint.norm; - -@SuppressWarnings("serial") - -/** - * Generic unchecked database exception. - */ -public class DbException extends RuntimeException { - - private String sql; - - public DbException() {} - - public DbException(String msg) { - super(msg); - } - - public DbException(Throwable t) { - super(t); - } - - public DbException(String msg, Throwable t) { - super(msg, t); - } - - public void setSql(String sql) { - this.sql = sql; - } - - public String getSql() { - return sql; - } - -} +package com.dieselpoint.norm; + +@SuppressWarnings("serial") + +/** + * Generic unchecked database exception. + */ +public class DbException extends RuntimeException { + + private String sql; + + public DbException() {} + + public DbException(String msg) { + super(msg); + } + + public DbException(Throwable t) { + super(t); + } + + public DbException(String msg, Throwable t) { + super(msg, t); + } + + public void setSql(String sql) { + this.sql = sql; + } + + public String getSql() { + return sql; + } + +} diff --git a/src/main/java/com/dieselpoint/norm/Query.java b/src/main/java/com/dieselpoint/norm/Query.java index 47ddde3..e32bc32 100644 --- a/src/main/java/com/dieselpoint/norm/Query.java +++ b/src/main/java/com/dieselpoint/norm/Query.java @@ -45,14 +45,14 @@ public class Query { public Query(Database db) { this.db = db; - this.sqlMaker = db.getSqlMaker(); - this.maxLatency( db.getMaxLatencyMillis() ); + sqlMaker = db.getSqlMaker(); + maxLatency( db.getMaxLatencyMillis() ); } /** * Add a where clause and some parameters to a query. Has no effect if the * .sql() method is used. - * + * * @param where Example: "name=?" * @param args The parameter values to use in the where, example: "Bob" */ @@ -65,7 +65,7 @@ public Query where(String where, Object... args) { /** * Create a query using straight SQL. Overrides any other methods like .where(), * .orderBy(), etc. - * + * * @param sql The SQL string to use, may include ? parameters. * @param args The parameter values to use in the query. */ @@ -78,7 +78,7 @@ public Query sql(String sql, Object... args) { /** * Create a query using straight SQL. Overrides any other methods like .where(), * .orderBy(), etc. - * + * * @param sql The SQL string to use, may include ? parameters. * @param args The parameter values to use in the query. */ @@ -104,9 +104,8 @@ public T first(Class clazz) { List list = results(clazz); if (!list.isEmpty()) { return list.get(0); - } else { - return null; } + return null; } /** @@ -261,12 +260,12 @@ private void close(AutoCloseable ac) { */ public Query insert(Object row) { - if (this.generatedKeyReceiver == null) { + if (generatedKeyReceiver == null) { PojoInfo pojoInfo = sqlMaker.getPojoInfo(row.getClass()); String[] names = pojoInfo.getGeneratedColumnNames(); if (names.length != 0) { - this.generatedKeyReceiver = row; - this.generatedKeyNames = names; + generatedKeyReceiver = row; + generatedKeyNames = names; } } @@ -420,31 +419,31 @@ private void populateGeneratedKeys(PreparedStatement state, Object generatedKeyR pojoInfo.putValue(generatedKeyReceiver, generatedKeyName, value); /*- - - + + Property prop = pojoInfo.getProperty(generatedKeyName); if (prop == null) { throw new DbException("Generated key name not found: " + generatedKeyName); } - + /* * getObject() below doesn't handle primitives correctly. Must convert to object * equivalent. * / - + Class type = Util.wrap(prop.dataType); - + Object colValue = sqlMaker.convertValue(rs.getObject(i), meta.getColumnTypeName(i)); - + Object newKey; if (colCount == 1) { newKey = rs.getObject(1, type); } else { newKey = rs.getObject(prop.name, type); } - + pojoInfo.putValue(generatedKeyReceiver, prop.name, newKey); - */ + */ } } } @@ -483,19 +482,19 @@ public Query generatedKeyReceiver(Object generatedKeyReceiver, String... generat /** * Temporary hack. Avoid. - * + * * @deprecated * @param generatedKeyName * @return / public Query generatedKeyName(String generatedKeyName) { * this.generatedKeyName = generatedKeyName; return this; } - * + * * public long getGeneratedKeyValue() { return generatedKeyValue; } */ /** * This is a temporary hack to deal with inserting Maps using sql. May go away. * Marking this deprecated right from the start. - * + * * @deprecated * @return / public long getGeneratedKey(String colName) { if (generatedKeys != * null) { try { return generatedKeys.getLong(colName); } catch @@ -563,7 +562,7 @@ public int getRowsAffected() { * Specify that this query should be a part of the specified transaction. */ public Query transaction(Transaction trans) { - this.transaction = trans; + transaction = trans; return this; } @@ -594,7 +593,7 @@ public ResultSetMetaData getResultSetMetaData() { * @return this, to enable maxLatency to be chained, a la {@code db.sql( "select count(*) from Thing" ).maxLatency(50).first( Long.class )} */ public Query maxLatency( long millis ) { - this.maxLatency = millis; + maxLatency = millis; return this; } diff --git a/src/main/java/com/dieselpoint/norm/Transaction.java b/src/main/java/com/dieselpoint/norm/Transaction.java index a7db549..3bfffd6 100644 --- a/src/main/java/com/dieselpoint/norm/Transaction.java +++ b/src/main/java/com/dieselpoint/norm/Transaction.java @@ -1,110 +1,110 @@ -package com.dieselpoint.norm; - -import com.dieselpoint.norm.latency.LatencyTimer; - -import java.io.Closeable; -import java.io.IOException; -import java.sql.Connection; - -/** - * Represents a database transaction. Create it using Transaction trans = - * Database.startTransation(), pass it to the query object using - * .transaction(trans), and then call trans.commit() or trans.rollback(). - *

- * Some things to note: commit() and rollback() also call close() on the - * connection, so this class cannot be reused after the transaction is committed - * or rolled back. - *

- *

- * This is just a convenience class. If the implementation is too restrictive, - * then you can manage your own transactions by calling Database.getConnection() - * and operate on the Connection directly. - *

- */ -public class Transaction implements Closeable { - private Connection con; - private Database db; - private long maxLatency; - - Transaction() { - this.maxLatency = -1; - } - - // package-private - Transaction( Database db, Connection con ) throws DbException { - this.db = db; - this.con = con; - this.maxLatency = db.getMaxLatencyMillis(); - setConnection( con ); - } - - // package-private - void setConnection(Connection con) throws DbException { - this.con = con; - try { - con.setAutoCommit(false); - } catch (Throwable t) { - throw new DbException(t); - } - } - - public void commit() { - try { - LatencyTimer myLatencyTimer = new LatencyTimer( this ); - con.commit(); - myLatencyTimer.stop( this ); - } catch (Throwable t) { - throw new DbException(t); - } finally { - try { - con.close(); - } catch (Throwable t) { - throw new DbException(t); - } - } - } - - public void rollback() { - try { - con.rollback(); - } catch (Throwable t) { - throw new DbException(t); - } finally { - try { - con.close(); - } catch (Throwable t) { - throw new DbException(t); - } - } - } - - public Connection getConnection() { - return con; - } - - /** - * This simply calls .commit(); - */ - @Override - public void close() throws IOException { - commit(); - } - - public Database getDatabase() { return db; } - - /** - * sets the maximum acceptable latency for this transaction. Must be called before {@link #commit()}. - *
If latency of the query exceeds the threshold then the - * {@link com.dieselpoint.norm.latency.LatencyAlerter} that have been added to the - * {@link Database} will be called in order. - * @param millis maximum number of milliseconds that a query can take to execute before an alert will be generated - * @return {@code this}, to enable maxLatency to be chained, a la {@code trans.maxLatency("Ten People Transaction", 50).commit()} - */ - public Transaction maxLatency( long millis ) { - this.maxLatency = millis; - return this; - } - - public long getMaxLatencyMillis() { return maxLatency; } - -} +package com.dieselpoint.norm; + +import java.io.Closeable; +import java.io.IOException; +import java.sql.Connection; + +import com.dieselpoint.norm.latency.LatencyTimer; + +/** + * Represents a database transaction. Create it using Transaction trans = + * Database.startTransation(), pass it to the query object using + * .transaction(trans), and then call trans.commit() or trans.rollback(). + *

+ * Some things to note: commit() and rollback() also call close() on the + * connection, so this class cannot be reused after the transaction is committed + * or rolled back. + *

+ *

+ * This is just a convenience class. If the implementation is too restrictive, + * then you can manage your own transactions by calling Database.getConnection() + * and operate on the Connection directly. + *

+ */ +public class Transaction implements Closeable { + private Connection con; + private Database db; + private long maxLatency; + + Transaction() { + maxLatency = -1; + } + + // package-private + Transaction( Database db, Connection con ) throws DbException { + this.db = db; + this.con = con; + maxLatency = db.getMaxLatencyMillis(); + setConnection( con ); + } + + // package-private + void setConnection(Connection con) throws DbException { + this.con = con; + try { + con.setAutoCommit(false); + } catch (Throwable t) { + throw new DbException(t); + } + } + + public void commit() { + try { + LatencyTimer myLatencyTimer = new LatencyTimer( this ); + con.commit(); + myLatencyTimer.stop( this ); + } catch (Throwable t) { + throw new DbException(t); + } finally { + try { + con.close(); + } catch (Throwable t) { + throw new DbException(t); + } + } + } + + public void rollback() { + try { + con.rollback(); + } catch (Throwable t) { + throw new DbException(t); + } finally { + try { + con.close(); + } catch (Throwable t) { + throw new DbException(t); + } + } + } + + public Connection getConnection() { + return con; + } + + /** + * This simply calls .commit(); + */ + @Override + public void close() throws IOException { + commit(); + } + + public Database getDatabase() { return db; } + + /** + * sets the maximum acceptable latency for this transaction. Must be called before {@link #commit()}. + *
If latency of the query exceeds the threshold then the + * {@link com.dieselpoint.norm.latency.LatencyAlerter} that have been added to the + * {@link Database} will be called in order. + * @param millis maximum number of milliseconds that a query can take to execute before an alert will be generated + * @return {@code this}, to enable maxLatency to be chained, a la {@code trans.maxLatency("Ten People Transaction", 50).commit()} + */ + public Transaction maxLatency( long millis ) { + maxLatency = millis; + return this; + } + + public long getMaxLatencyMillis() { return maxLatency; } + +} diff --git a/src/main/java/com/dieselpoint/norm/Util.java b/src/main/java/com/dieselpoint/norm/Util.java index b90a78b..7d88fcf 100644 --- a/src/main/java/com/dieselpoint/norm/Util.java +++ b/src/main/java/com/dieselpoint/norm/Util.java @@ -1,87 +1,123 @@ -package com.dieselpoint.norm; - -import java.util.Collection; - -public class Util { - - public static String join(String[] strs) { - StringBuilder buf = new StringBuilder(); - for (int i = 0; i < strs.length; i++) { - if (i > 0) { - buf.append(","); - } - buf.append(strs[i]); - } - return buf.toString(); - } - - public static String join(Collection strs) { - StringBuilder sb = new StringBuilder(); - boolean first = true; - for (String col : strs) { - if (first) { - first = false; - } else { - sb.append(","); - } - sb.append(col); - } - return sb.toString(); - } - - public static String getQuestionMarks(int count) { - StringBuilder sb = new StringBuilder(count * 2); - for (int i = 0; i < count; i++) { - if (i > 0) { - sb.append(','); - } - sb.append('?'); - } - return sb.toString(); - } - - public static boolean isPrimitiveOrString(Class c) { - if (c.isPrimitive()) { - return true; - } else if (c == Byte.class || c == Short.class || c == Integer.class || c == Long.class || c == Float.class - || c == Double.class || c == Boolean.class || c == Character.class || c == String.class) { - return true; - } else { - return false; - } - } - - public static Class wrap(Class type) { - if (!type.isPrimitive()) { - return type; - } - if (type == int.class) { - return Integer.class; - } - if (type == long.class) { - return Long.class; - } - if (type == boolean.class) { - return Boolean.class; - } - if (type == byte.class) { - return Byte.class; - } - if (type == char.class) { - return Character.class; - } - if (type == double.class) { - return Double.class; - } - if (type == float.class) { - return Float.class; - } - if (type == short.class) { - return Short.class; - } - if (type == void.class) { - return Void.class; - } - throw new RuntimeException("Will never get here"); - } -} +package com.dieselpoint.norm; + +import java.util.List; + +public class Util { + + /** + * @param objs | Objects to join with ',' delimiter. + * + * @return Same as {@link String#join(CharSequence, CharSequence...)} only for ant {@link Object}! + */ + public static String join(Object[] objs) + { + return join(objs, ','); + } + + /** + * @param objs | Objects to join with ',' delimeter. + * @param delimeter | delimiter to separate objects with + * + * @return Same as {@link String#join(CharSequence, CharSequence...)} only for ant {@link Object}! + */ + public static String join(Object[] objs, char delimiter) { + StringBuilder buf = new StringBuilder(); + for (int i = 0, size = objs.length; i < size; i++) { + if (i > 0) + buf.append(delimiter); + + buf.append(objs[i]); + } + + return buf.toString(); + } + + /** + * @param objs | Objects to join with ',' delimiter. + * + * @return Same as {@link String#join(CharSequence, CharSequence...)} only for ant {@link Object}! + */ + public static String join(List strs) + { + return join(strs, ','); + } + + /** + * @param objs | Objects to join with ',' delimeter. + * @param delimeter | delimiter to separate objects with + * + * @return Same as {@link String#join(CharSequence, CharSequence...)} only for ant {@link Object}! + */ + public static String join(List objs, char delimeter) { + StringBuilder sb = new StringBuilder(); + for (int i = 0, size = objs.size(); i < size; i++) { + if (i > 0) { + sb.append(delimeter); + } + sb.append(objs.get(i)); + } + return sb.toString(); + } + + /** + * @param ch | Char to join count times. + * @param count | How many times to join ch. + * + * @return ch separated with ',' as delimiter counr times. For example joinChars('?', 3) will return "?,?,?"! + */ + public static String joinChars(char ch, int count) { + StringBuilder sb = new StringBuilder(count * 2); + for (int i = 0; i < count; i++) { + if (i > 0) + sb.append(','); + + sb.append(ch); + } + return sb.toString(); + } + + /** + * @return True if c is primitive datatype or {@link String}! + */ + public static boolean isPrimitiveOrString(Class c) { + if (c.isPrimitive()) + return true; + + return c == Byte.class || c == Short.class || c == Integer.class || c == Long.class || c == Float.class || + c == Double.class || c == Boolean.class || c == Character.class || c == String.class; + } + + /** + * @return Type class "wrappered" to its wrapper type. If class is not of primitive type then return type... + */ + public static Class wrap(Class type) { // ? NOT USED IN LIBRARY + if (type == int.class) { + return Integer.class; + } + if (type == long.class) { + return Long.class; + } + if (type == boolean.class) { + return Boolean.class; + } + if (type == byte.class) { + return Byte.class; + } + if (type == char.class) { + return Character.class; + } + if (type == double.class) { + return Double.class; + } + if (type == float.class) { + return Float.class; + } + if (type == short.class) { + return Short.class; + } + if (type == void.class) { + return Void.class; + } + return type; + } +} diff --git a/src/main/java/com/dieselpoint/norm/converter/IntArrayToListConverter.java b/src/main/java/com/dieselpoint/norm/converter/IntArrayToListConverter.java index 649345a..912d81c 100644 --- a/src/main/java/com/dieselpoint/norm/converter/IntArrayToListConverter.java +++ b/src/main/java/com/dieselpoint/norm/converter/IntArrayToListConverter.java @@ -3,6 +3,7 @@ import java.sql.Array; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.persistence.AttributeConverter; @@ -25,13 +26,8 @@ public List convertToEntityAttribute(Array dbData) { if (dbData.getBaseType() != java.sql.Types.INTEGER) { throw new DbException("Database is not returning an integer array"); } - - Integer [] arr = (Integer[]) dbData.getArray(); - List out = new ArrayList<>(); - for (Integer i: arr) { - out.add(i); - } - return out; + + return new ArrayList<>(Arrays.asList((Integer[]) dbData.getArray())); } catch (SQLException e) { throw new DbException(e); diff --git a/src/main/java/com/dieselpoint/norm/converter/SimpleArray.java b/src/main/java/com/dieselpoint/norm/converter/SimpleArray.java index 6465ea9..dcb36c9 100644 --- a/src/main/java/com/dieselpoint/norm/converter/SimpleArray.java +++ b/src/main/java/com/dieselpoint/norm/converter/SimpleArray.java @@ -5,10 +5,10 @@ import java.util.Map; public class SimpleArray implements java.sql.Array { - + private int baseType; private Object [] arr; - + public SimpleArray(int baseType, Object [] arr) { this.baseType = baseType; this.arr = arr; diff --git a/src/main/java/com/dieselpoint/norm/converter/StringToIntListConverter.java b/src/main/java/com/dieselpoint/norm/converter/StringToIntListConverter.java index 7386854..78b21a5 100644 --- a/src/main/java/com/dieselpoint/norm/converter/StringToIntListConverter.java +++ b/src/main/java/com/dieselpoint/norm/converter/StringToIntListConverter.java @@ -6,6 +6,8 @@ import javax.persistence.AttributeConverter; import javax.persistence.Converter; +import com.dieselpoint.norm.Util; + @Converter public class StringToIntListConverter implements AttributeConverter, String> { @@ -14,32 +16,24 @@ public String convertToDatabaseColumn(List attribute) { if (attribute == null) { return null; } - StringBuilder sb = new StringBuilder(); - int len = attribute.size(); - - for (int i = 0; i < len; i++) { - if (i > 0) { - sb.append(','); - } - sb.append(attribute.get(i).intValue()); - - } - return sb.toString(); + + return Util.join(attribute); } @Override public List convertToEntityAttribute(String in) { // deserialize string in the form "123,456" no spaces allowed List list = new ArrayList<>(); - if (in == null || in.length() == 0) { + int len; + if (in == null || (len = in.length()) == 0) { return list; } int value = 0; - for (int i = 0; i < in.length(); i++) { + for (int i = 0; i < len; i++) { int digit = in.charAt(i) - '0'; if (digit >= 0 && digit <= 9) { - value = (value * 10) + digit; + value = value * 10 + digit; } else { // hit a comma list.add(value); diff --git a/src/main/java/com/dieselpoint/norm/latency/BackoffLatencyAlerter.java b/src/main/java/com/dieselpoint/norm/latency/BackoffLatencyAlerter.java index 5f62a2d..2019ef6 100644 --- a/src/main/java/com/dieselpoint/norm/latency/BackoffLatencyAlerter.java +++ b/src/main/java/com/dieselpoint/norm/latency/BackoffLatencyAlerter.java @@ -1,11 +1,11 @@ package com.dieselpoint.norm.latency; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.time.Duration; import java.util.Random; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * One of the dangers when reporting latency issues to external services, is that the reporting itself a) takes a * significant amount of time and may create Customer Experience issues, and b) you end up with millions of latency @@ -24,60 +24,60 @@ */ @SuppressWarnings( "unused" ) public abstract class BackoffLatencyAlerter implements LatencyAlerter { - private static final Logger logger = LoggerFactory.getLogger( BackoffLatencyAlerter.class ); + private static final Logger logger = LoggerFactory.getLogger( BackoffLatencyAlerter.class ); - private final long minimumReportingLatencyMillis, maximumReportingIntervalMillis; - private long nextReportTime; - private double backoffs; - private long alertsSwallowedWhileWaiting = 0; - private final Random random = new Random(); + private final long minimumReportingLatencyMillis, maximumReportingIntervalMillis; + private long nextReportTime; + private double backoffs; + private long alertsSwallowedWhileWaiting = 0; + private final Random random = new Random(); - public BackoffLatencyAlerter( Duration minimumReportingInterval, Duration maximumReportingInterval ) { - this.minimumReportingLatencyMillis = minimumReportingInterval.toMillis(); - this.maximumReportingIntervalMillis = maximumReportingInterval.toMillis(); - this.backoffs = 1; - this.nextReportTime = System.currentTimeMillis(); - } + public BackoffLatencyAlerter( Duration minimumReportingInterval, Duration maximumReportingInterval ) { + minimumReportingLatencyMillis = minimumReportingInterval.toMillis(); + maximumReportingIntervalMillis = maximumReportingInterval.toMillis(); + backoffs = 1; + nextReportTime = System.currentTimeMillis(); + } - private long calculateWaitTime() { - backoffs += (alertsSwallowedWhileWaiting > 0) ? 1 : -0.25; // come back down much more slowly than we went up - backoffs = backoffs < 1 ? 1 : backoffs; + private long calculateWaitTime() { + backoffs += alertsSwallowedWhileWaiting > 0 ? 1 : -0.25; // come back down much more slowly than we went up + backoffs = backoffs < 1 ? 1 : backoffs; - // timeToWait = (base * 2^n) +/- (jitter) - long jitter = (minimumReportingLatencyMillis/2) - (random.nextLong() % minimumReportingLatencyMillis); - long timeToWait = (minimumReportingLatencyMillis * (long)Math.pow(2, backoffs) ) + jitter; - if (timeToWait > maximumReportingIntervalMillis) { - // timeToWait = maximumReportingIntervalMillis; - NO, we still want the jitter included - --backoffs; - } - return timeToWait; - } + // timeToWait = (base * 2^n) +/- (jitter) + long jitter = minimumReportingLatencyMillis/2 - random.nextLong() % minimumReportingLatencyMillis; + long timeToWait = minimumReportingLatencyMillis * (long)Math.pow(2, backoffs) + jitter; + if (timeToWait > maximumReportingIntervalMillis) { + // timeToWait = maximumReportingIntervalMillis; - NO, we still want the jitter included + --backoffs; + } + return timeToWait; + } - @Override - public synchronized void alertLatencyFailure( DbLatencyWarning warning ) { - if (warning.maxAcceptableLatency != 0) { - long myTime = System.currentTimeMillis(); - if (nextReportTime <= myTime) { - if (alertLatencyFailureAfterBackoffAndJitter( warning, alertsSwallowedWhileWaiting ) == false) - ++alertsSwallowedWhileWaiting; - nextReportTime = myTime + calculateWaitTime(); - alertsSwallowedWhileWaiting = 0; - } - else { - logger.info( "Swallowed latency failure:" + warning ); - ++alertsSwallowedWhileWaiting; - } - } - } + @Override + public synchronized void alertLatencyFailure( DbLatencyWarning warning ) { + if (warning.maxAcceptableLatency != 0) { + long myTime = System.currentTimeMillis(); + if (nextReportTime <= myTime) { + if (!alertLatencyFailureAfterBackoffAndJitter( warning, alertsSwallowedWhileWaiting )) + ++alertsSwallowedWhileWaiting; + nextReportTime = myTime + calculateWaitTime(); + alertsSwallowedWhileWaiting = 0; + } + else { + logger.info( "Swallowed latency failure:" + warning ); + ++alertsSwallowedWhileWaiting; + } + } + } - /** - * @param warning the latency warning - * @param numberOfAlertsSwallowed the number of alerts that were swallowed during the exponential backoff period. This - * might (or might not) be interesting to report alongside the current issue. It'll - * definitely give you a sense of how bad things have gone! - * @return true if notifying the remote service was successful, false otherwise. If false, then we'll automatically - * backoff calls to reporting in the same way as latency failures, to avoid a slowdown / issue on - * monitoring impacting the actual customer experience - */ - public abstract boolean alertLatencyFailureAfterBackoffAndJitter( DbLatencyWarning warning, long numberOfAlertsSwallowed ); + /** + * @param warning the latency warning + * @param numberOfAlertsSwallowed the number of alerts that were swallowed during the exponential backoff period. This + * might (or might not) be interesting to report alongside the current issue. It'll + * definitely give you a sense of how bad things have gone! + * @return true if notifying the remote service was successful, false otherwise. If false, then we'll automatically + * backoff calls to reporting in the same way as latency failures, to avoid a slowdown / issue on + * monitoring impacting the actual customer experience + */ + public abstract boolean alertLatencyFailureAfterBackoffAndJitter( DbLatencyWarning warning, long numberOfAlertsSwallowed ); } diff --git a/src/main/java/com/dieselpoint/norm/latency/DbLatencyWarning.java b/src/main/java/com/dieselpoint/norm/latency/DbLatencyWarning.java index f556f40..ed1b51d 100644 --- a/src/main/java/com/dieselpoint/norm/latency/DbLatencyWarning.java +++ b/src/main/java/com/dieselpoint/norm/latency/DbLatencyWarning.java @@ -1,9 +1,9 @@ package com.dieselpoint.norm.latency; -import com.dieselpoint.norm.Transaction; - import java.util.Arrays; +import com.dieselpoint.norm.Transaction; + /** * An exception-like class, that makes it easy to pass the messages, and stack trace associated * with a {@link com.dieselpoint.norm.Query} or {@link Transaction#commit()} database call that has @@ -11,46 +11,47 @@ * the system administrators to the warning */ public class DbLatencyWarning { - public final long maxAcceptableLatency; - public final long actualLatency; - public final String cause; - public final String offendingStatement; - - protected DbLatencyWarning( long maxAcceptableLatency, long actualLatency, String cause ) { - this.maxAcceptableLatency = maxAcceptableLatency; - this.actualLatency = actualLatency; - this.cause = cause; - this.offendingStatement = getOffendingStatement(); - } - - public DbLatencyWarning(long maxAcceptableLatency, long actualLatency, String theNaughtySql, Object[] theNaughtyArgs ) { - this( maxAcceptableLatency, actualLatency, - "SQL:" + theNaughtySql + ", SQL_Args:" + Arrays.deepToString(theNaughtyArgs) ); - } - - public DbLatencyWarning(long maxAcceptableLatency, long actualLatency, Transaction theNaughtyTransaction ) { - this( maxAcceptableLatency, actualLatency, "Transaction commit exceeded threshold:" ); - } - - /** - * @return the most recent call on the stack before any call to classes in the {@code com.dieselpoint.norm} package. - * This ought to pinpoint the call that exceeded the latency threshold. Returns {@code "[Unknown]"}, when it can't - * figure out the caller, i.e. will never return null - */ - private String getOffendingStatement() { - StackTraceElement[] elements = Thread.currentThread().getStackTrace(); - for (int i=2; i 0) - throw new DbException( warning.toString() ); - } + @Override + public void alertLatencyFailure( DbLatencyWarning warning ) { + if (warning.maxAcceptableLatency > 0) + throw new DbException( warning.toString() ); + } } \ No newline at end of file diff --git a/src/main/java/com/dieselpoint/norm/latency/LatencyAlerter.java b/src/main/java/com/dieselpoint/norm/latency/LatencyAlerter.java index c8efedd..98e3c3c 100644 --- a/src/main/java/com/dieselpoint/norm/latency/LatencyAlerter.java +++ b/src/main/java/com/dieselpoint/norm/latency/LatencyAlerter.java @@ -7,5 +7,5 @@ * it should be trivial to forward the latency alert to e.g. AWS Cloudwatch, PagerDuty, or honeybadger.io */ public interface LatencyAlerter { - public void alertLatencyFailure( DbLatencyWarning warning ); + void alertLatencyFailure( DbLatencyWarning warning ); } diff --git a/src/main/java/com/dieselpoint/norm/latency/LatencyTimer.java b/src/main/java/com/dieselpoint/norm/latency/LatencyTimer.java index 7ea48da..7cd69aa 100644 --- a/src/main/java/com/dieselpoint/norm/latency/LatencyTimer.java +++ b/src/main/java/com/dieselpoint/norm/latency/LatencyTimer.java @@ -8,52 +8,52 @@ * Utility class that abstracts the starting / stopping of timers and checking whether sql duration was within threshold */ public class LatencyTimer { - public final long startMillis; - public long duration; - public final long maxAcceptableLatency; - public final Database db; - - public LatencyTimer( Query query ) { - this.db = query.getDatabase(); - startMillis = System.currentTimeMillis(); - maxAcceptableLatency = query.getMaxLatencyMillis(); - } - - public LatencyTimer( Transaction transaction ) { - this.db = transaction.getDatabase(); - startMillis = System.currentTimeMillis(); - maxAcceptableLatency = transaction.getMaxLatencyMillis(); - } - - /** - * @return true if latency was within acceptable bounds, or maxAcceptableLatency is negative - */ - private boolean stop() { - if (maxAcceptableLatency < 0) - return true; - duration = System.currentTimeMillis() - startMillis; - if (maxAcceptableLatency == 0) - return false; - return duration <= maxAcceptableLatency; - } - - public boolean stop( String sql, Object[] args ) { - if (stop() == false) { - if (db != null) { - db.alertLatency( new DbLatencyWarning( maxAcceptableLatency, duration, sql, args ) ); - } - } - return false; - } - - public boolean stop( Transaction aTransaction ) { - if (stop() == false) { - if (db != null) { - db.alertLatency( new DbLatencyWarning( maxAcceptableLatency, duration, aTransaction ) ); - } - } - return false; - } + public final long startMillis; + public long duration; + public final long maxAcceptableLatency; + public final Database db; + + public LatencyTimer( Query query ) { + db = query.getDatabase(); + startMillis = System.currentTimeMillis(); + maxAcceptableLatency = query.getMaxLatencyMillis(); + } + + public LatencyTimer( Transaction transaction ) { + db = transaction.getDatabase(); + startMillis = System.currentTimeMillis(); + maxAcceptableLatency = transaction.getMaxLatencyMillis(); + } + + /** + * @return true if latency was within acceptable bounds, or maxAcceptableLatency is negative + */ + private boolean stop() { + if (maxAcceptableLatency < 0) + return true; + duration = System.currentTimeMillis() - startMillis; + if (maxAcceptableLatency == 0) + return false; + return duration <= maxAcceptableLatency; + } + + public boolean stop( String sql, Object[] args ) { + if (!stop()) { + if (db != null) { + db.alertLatency( new DbLatencyWarning( maxAcceptableLatency, duration, sql, args ) ); + } + } + return false; + } + + public boolean stop( Transaction aTransaction ) { + if (!stop()) { + if (db != null) { + db.alertLatency( new DbLatencyWarning( maxAcceptableLatency, duration, aTransaction ) ); + } + } + return false; + } } diff --git a/src/main/java/com/dieselpoint/norm/latency/Slf4jLatencyAlerter.java b/src/main/java/com/dieselpoint/norm/latency/Slf4jLatencyAlerter.java index 2711671..06468a3 100644 --- a/src/main/java/com/dieselpoint/norm/latency/Slf4jLatencyAlerter.java +++ b/src/main/java/com/dieselpoint/norm/latency/Slf4jLatencyAlerter.java @@ -4,22 +4,22 @@ import org.slf4j.LoggerFactory; public class Slf4jLatencyAlerter implements LatencyAlerter { - private static final Logger logger = LoggerFactory.getLogger( Slf4jLatencyAlerter.class ); - private final Logger instanceLogger; + private static final Logger logger = LoggerFactory.getLogger( Slf4jLatencyAlerter.class ); + private final Logger instanceLogger; - public Slf4jLatencyAlerter() { - this( logger ); - } + public Slf4jLatencyAlerter() { + this( logger ); + } - public Slf4jLatencyAlerter( Logger theLoggerToUse ) { - instanceLogger = theLoggerToUse; - } + public Slf4jLatencyAlerter( Logger theLoggerToUse ) { + instanceLogger = theLoggerToUse; + } - @Override - public void alertLatencyFailure( DbLatencyWarning warning ) { - if (warning.maxAcceptableLatency == 0) - instanceLogger.info( warning.toString() ); - else - instanceLogger.warn( warning.toString() ); - } + @Override + public void alertLatencyFailure( DbLatencyWarning warning ) { + if (warning.maxAcceptableLatency == 0) + instanceLogger.info( warning.toString() ); + else + instanceLogger.warn( warning.toString() ); + } } diff --git a/src/main/java/com/dieselpoint/norm/latency/StdoutLatencyAlerter.java b/src/main/java/com/dieselpoint/norm/latency/StdoutLatencyAlerter.java index 718b4b3..66f9cdf 100644 --- a/src/main/java/com/dieselpoint/norm/latency/StdoutLatencyAlerter.java +++ b/src/main/java/com/dieselpoint/norm/latency/StdoutLatencyAlerter.java @@ -2,11 +2,11 @@ public class StdoutLatencyAlerter implements LatencyAlerter { - public StdoutLatencyAlerter() { - } + public StdoutLatencyAlerter() { + } - @Override - public void alertLatencyFailure( DbLatencyWarning warning ) { - System.out.println( warning.toString() ); - } + @Override + public void alertLatencyFailure( DbLatencyWarning warning ) { + System.out.println( warning.toString() ); + } } diff --git a/src/main/java/com/dieselpoint/norm/serialize/DbSerializable.java b/src/main/java/com/dieselpoint/norm/serialize/DbSerializable.java index 66a791c..12e66d9 100644 --- a/src/main/java/com/dieselpoint/norm/serialize/DbSerializable.java +++ b/src/main/java/com/dieselpoint/norm/serialize/DbSerializable.java @@ -1,14 +1,14 @@ -package com.dieselpoint.norm.serialize; - - -/** - * Serializes a class to and from a string. - * Implementations must have a zero-arg constructor and must - * be thread-safe. - */ -public interface DbSerializable { - - public String serialize(Object in); - public Object deserialize(String in, Class targetClass); - -} +package com.dieselpoint.norm.serialize; + + +/** + * Serializes a class to and from a string. + * Implementations must have a zero-arg constructor and must + * be thread-safe. + */ +public interface DbSerializable { + + String serialize(Object in); + Object deserialize(String in, Class targetClass); + +} diff --git a/src/main/java/com/dieselpoint/norm/serialize/DbSerializer.java b/src/main/java/com/dieselpoint/norm/serialize/DbSerializer.java index 5751838..7c148cb 100644 --- a/src/main/java/com/dieselpoint/norm/serialize/DbSerializer.java +++ b/src/main/java/com/dieselpoint/norm/serialize/DbSerializer.java @@ -1,14 +1,14 @@ -package com.dieselpoint.norm.serialize; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.METHOD,ElementType.FIELD}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface DbSerializer { - Class value(); -} +package com.dieselpoint.norm.serialize; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD,ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DbSerializer { + Class value(); +} diff --git a/src/main/java/com/dieselpoint/norm/sqlmakers/MySqlMaker.java b/src/main/java/com/dieselpoint/norm/sqlmakers/MySqlMaker.java index 107e07e..135ee53 100644 --- a/src/main/java/com/dieselpoint/norm/sqlmakers/MySqlMaker.java +++ b/src/main/java/com/dieselpoint/norm/sqlmakers/MySqlMaker.java @@ -1,79 +1,79 @@ -package com.dieselpoint.norm.sqlmakers; - -import com.dieselpoint.norm.Query; - -import java.util.Objects; - - -public class MySqlMaker extends StandardSqlMaker { - - @Override - public String getUpsertSql(Query query, Object row) { - StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); - return String.format(pojoInfo.upsertSql, Objects.requireNonNullElse(query.getTable(), pojoInfo.table)); - } - - @Override - public Object[] getUpsertArgs(Query query, Object row) { - - // same args as insert, but we need to duplicate the values - Object [] args = super.getInsertArgs(query, row); - - int count = args.length; - - Object [] upsertArgs = new Object[count * 2]; - System.arraycopy(args, 0, upsertArgs, 0, count); - System.arraycopy(args, 0, upsertArgs, count, count); - - return upsertArgs; - } - - - @Override - public void makeUpsertSql(StandardPojoInfo pojoInfo) { - - // INSERT INTO table (a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1; - - // mostly the same as the makeInsertSql code - // it uses the same column names and argcount - - StringBuilder buf = new StringBuilder(); - buf.append(pojoInfo.insertSql); - buf.append(" on duplicate key update "); - - boolean first = true; - for (String colName: pojoInfo.insertColumnNames) { - if (first) { - first = false; - } else { - buf.append(','); - } - buf.append(colName); - buf.append("=?"); - } - - pojoInfo.upsertSql = buf.toString(); - } - - @Override - protected String getColType(Class dataType, int length, int precision, int scale) { - String colType; - - if (dataType.equals(Boolean.class) || dataType.equals(boolean.class)) { - colType = "tinyint"; - } else { - colType = super.getColType(dataType, length, precision, scale); - } - return colType; - } - - @Override - public Object convertValue(Object value, String columnTypeName) { - if ("TINYINT".equalsIgnoreCase(columnTypeName)) { - value = (int) value == 1; - } - - return value; - } - +package com.dieselpoint.norm.sqlmakers; + +import java.util.Objects; + +import com.dieselpoint.norm.Query; + + +public class MySqlMaker extends StandardSqlMaker { + + @Override + public String getUpsertSql(Query query, Object row) { + StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); + return String.format(pojoInfo.upsertSql, Objects.requireNonNullElse(query.getTable(), pojoInfo.table)); + } + + @Override + public Object[] getUpsertArgs(Query query, Object row) { + + // same args as insert, but we need to duplicate the values + Object [] args = super.getInsertArgs(query, row); + + int count = args.length; + + Object [] upsertArgs = new Object[count * 2]; + System.arraycopy(args, 0, upsertArgs, 0, count); + System.arraycopy(args, 0, upsertArgs, count, count); + + return upsertArgs; + } + + + @Override + public void makeUpsertSql(StandardPojoInfo pojoInfo) { + + // INSERT INTO table (a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1; + + // mostly the same as the makeInsertSql code + // it uses the same column names and argcount + + StringBuilder buf = new StringBuilder(); + buf.append(pojoInfo.insertSql); + buf.append(" on duplicate key update "); + + boolean first = true; + for (String colName: pojoInfo.insertColumnNames) { + if (first) { + first = false; + } else { + buf.append(','); + } + buf.append(colName); + buf.append("=?"); + } + + pojoInfo.upsertSql = buf.toString(); + } + + @Override + protected String getColType(Class dataType, int length, int precision, int scale) { + String colType; + + if (dataType.equals(Boolean.class) || dataType.equals(boolean.class)) { + colType = "tinyint"; + } else { + colType = super.getColType(dataType, length, precision, scale); + } + return colType; + } + + @Override + public Object convertValue(Object value, String columnTypeName) { + if ("TINYINT".equalsIgnoreCase(columnTypeName)) { + value = (int) value == 1; + } + + return value; + } + } \ No newline at end of file diff --git a/src/main/java/com/dieselpoint/norm/sqlmakers/PojoInfo.java b/src/main/java/com/dieselpoint/norm/sqlmakers/PojoInfo.java index f7ae10b..4f7cb28 100644 --- a/src/main/java/com/dieselpoint/norm/sqlmakers/PojoInfo.java +++ b/src/main/java/com/dieselpoint/norm/sqlmakers/PojoInfo.java @@ -1,15 +1,23 @@ -package com.dieselpoint.norm.sqlmakers; - -public interface PojoInfo { - - public Object getValue(Object pojo, String name); - - public void putValue(Object pojo, String name, Object value); - - public void putValue(Object pojo, String name, Object value, boolean ignoreIfMissing); - - public String[] getGeneratedColumnNames(); - - public Property getProperty(String name); - -} +package com.dieselpoint.norm.sqlmakers; + +/** + * Interface that provides basic means of getting information about Plain Old Java Object (pojo) and allows you to dynamically read and write into its fields. + * Used mainly internally for pojo to database entity/record mapping! + * + * @see SqlMaker + */ +public interface PojoInfo { + + Object getValue(Object pojo, String name); + + default void putValue(Object pojo, String name, Object value) { + putValue(pojo, name, value, false); + } + + void putValue(Object pojo, String name, Object value, boolean ignoreIfMissing); + + String[] getGeneratedColumnNames(); + + Property getProperty(String name); + +} diff --git a/src/main/java/com/dieselpoint/norm/sqlmakers/PostgresMaker.java b/src/main/java/com/dieselpoint/norm/sqlmakers/PostgresMaker.java index 0bcc909..dea4f46 100644 --- a/src/main/java/com/dieselpoint/norm/sqlmakers/PostgresMaker.java +++ b/src/main/java/com/dieselpoint/norm/sqlmakers/PostgresMaker.java @@ -2,21 +2,23 @@ import javax.persistence.Column; +import com.dieselpoint.norm.Util; + public class PostgresMaker extends StandardSqlMaker { @Override public String getCreateTableSql(Class clazz) { - + StringBuilder buf = new StringBuilder(); StandardPojoInfo pojoInfo = getPojoInfo(clazz); buf.append("create table "); buf.append(pojoInfo.table); buf.append(" ("); - + boolean needsComma = false; for (Property prop : pojoInfo.propertyMap.values()) { - + if (needsComma) { buf.append(','); } @@ -24,7 +26,7 @@ public String getCreateTableSql(Class clazz) { Column columnAnnot = prop.columnAnnotation; if (columnAnnot == null) { - + buf.append(prop.name); buf.append(" "); if (prop.isGenerated) { @@ -32,47 +34,38 @@ public String getCreateTableSql(Class clazz) { } else { buf.append(getColType(prop.dataType, 255, 10, 2)); } - + + } else if (columnAnnot.columnDefinition() == null) { + + // let the column def override everything + buf.append(columnAnnot.columnDefinition()); + } else { - if (columnAnnot.columnDefinition() == null) { - - // let the column def override everything - buf.append(columnAnnot.columnDefinition()); - + + buf.append(prop.name); + buf.append(" "); + if (prop.isGenerated) { + buf.append(" serial"); } else { + buf.append(getColType(prop.dataType, columnAnnot.length(), columnAnnot.precision(), columnAnnot.scale())); + } + + if (columnAnnot.unique()) { + buf.append(" unique"); + } - buf.append(prop.name); - buf.append(" "); - if (prop.isGenerated) { - buf.append(" serial"); - } else { - buf.append(getColType(prop.dataType, columnAnnot.length(), columnAnnot.precision(), columnAnnot.scale())); - } - - if (columnAnnot.unique()) { - buf.append(" unique"); - } - - if (!columnAnnot.nullable()) { - buf.append(" not null"); - } + if (!columnAnnot.nullable()) { + buf.append(" not null"); } } } - + if (pojoInfo.primaryKeyNames.size() > 0) { - buf.append(", primary key ("); - for(int i = 0; i < pojoInfo.primaryKeyNames.size(); i++){ - if(i > 0){ - buf.append(","); - } - buf.append(pojoInfo.primaryKeyNames.get(i)); - } - buf.append(")"); + buf.append(", primary key (").append(Util.join(pojoInfo.primaryKeyNames)).append(")"); } - + buf.append(")"); - + return buf.toString(); } diff --git a/src/main/java/com/dieselpoint/norm/sqlmakers/Property.java b/src/main/java/com/dieselpoint/norm/sqlmakers/Property.java index 5ab659a..68d012b 100644 --- a/src/main/java/com/dieselpoint/norm/sqlmakers/Property.java +++ b/src/main/java/com/dieselpoint/norm/sqlmakers/Property.java @@ -9,6 +9,12 @@ import com.dieselpoint.norm.serialize.DbSerializable; +/** + * Contains information about single field of java Object. + * It is used for both SQL generation and reading / writing into objects... + * + * @see PojoInfo + */ @SuppressWarnings("rawtypes") public class Property { public String name; @@ -23,5 +29,5 @@ public class Property { public EnumType enumType; public Column columnAnnotation; public DbSerializable serializer; - public AttributeConverter converter; + public AttributeConverter converter; } diff --git a/src/main/java/com/dieselpoint/norm/sqlmakers/SqlMaker.java b/src/main/java/com/dieselpoint/norm/sqlmakers/SqlMaker.java index 86e4d9e..245fccd 100644 --- a/src/main/java/com/dieselpoint/norm/sqlmakers/SqlMaker.java +++ b/src/main/java/com/dieselpoint/norm/sqlmakers/SqlMaker.java @@ -1,31 +1,38 @@ -package com.dieselpoint.norm.sqlmakers; - -import com.dieselpoint.norm.Query; - -public interface SqlMaker { - - public String getInsertSql(Query query, Object row); - - public Object[] getInsertArgs(Query query, Object row); - - public String getUpdateSql(Query query, Object row); - - public Object[] getUpdateArgs(Query query, Object row); - - public String getDeleteSql(Query query, Object row); - - public Object[] getDeleteArgs(Query query, Object row); - - public String getUpsertSql(Query query, Object row); - - public Object[] getUpsertArgs(Query query, Object row); - - public String getSelectSql(Query query, Class rowClass); - - public String getCreateTableSql(Class clazz); - - public PojoInfo getPojoInfo(Class rowClass); - - public Object convertValue(Object value, String columnTypeName); - -} +package com.dieselpoint.norm.sqlmakers; + +import com.dieselpoint.norm.Query; + +/** + * Used for generating real SQL queries from {@link Query} and respective pojo representing entity/database record. + * + * Implementing this interface is the most low level of creating your own SQL flawor implementation. + * + * @see StandardSqlMaker + */ +public interface SqlMaker { + + String getInsertSql(Query query, Object row); + + Object[] getInsertArgs(Query query, Object row); + + String getUpdateSql(Query query, Object row); + + Object[] getUpdateArgs(Query query, Object row); + + String getDeleteSql(Query query, Object row); + + Object[] getDeleteArgs(Query query, Object row); + + String getUpsertSql(Query query, Object row); + + Object[] getUpsertArgs(Query query, Object row); + + String getSelectSql(Query query, Class rowClass); + + String getCreateTableSql(Class clazz); + + PojoInfo getPojoInfo(Class rowClass); + + Object convertValue(Object value, String columnTypeName); + +} diff --git a/src/main/java/com/dieselpoint/norm/sqlmakers/StandardPojoInfo.java b/src/main/java/com/dieselpoint/norm/sqlmakers/StandardPojoInfo.java index 26b2875..3e2f35d 100644 --- a/src/main/java/com/dieselpoint/norm/sqlmakers/StandardPojoInfo.java +++ b/src/main/java/com/dieselpoint/norm/sqlmakers/StandardPojoInfo.java @@ -32,7 +32,6 @@ /** * Provides means of reading and writing properties in a pojo. */ -@SuppressWarnings("rawtypes") public class StandardPojoInfo implements PojoInfo { /* @@ -74,9 +73,9 @@ public StandardPojoInfo(Class clazz) { // reorder the properties String[] cols = colOrder.value(); List reordered = new ArrayList<>(); - for (int i = 0; i < cols.length; i++) { + for (String col : cols) { for (Property prop : props) { - if (prop.name.equals(cols[i])) { + if (prop.name.equals(col)) { reordered.add(prop); break; } @@ -89,7 +88,7 @@ public StandardPojoInfo(Class clazz) { for (Property prop : props) { if (propertyMap.put(prop.name, prop) != null) { throw new DbException("Duplicate pojo property found: '" + prop.name + "' in " + clazz.getName() - + ". There may be both a field and a getter/setter"); + + ". There may be both a field and a getter/setter"); } } } @@ -169,15 +168,15 @@ private List populateProperties(Class clazz) } } - this.generatedColumnNames = new String[genCols.size()]; - genCols.toArray(this.generatedColumnNames); + generatedColumnNames = new String[genCols.size()]; + genCols.toArray(generatedColumnNames); return props; } /** * Apply the annotations on the field or getter method to the property. - * + * * @throws IllegalAccessException * @throws InstantiationException */ @@ -226,6 +225,7 @@ private void applyAnnotations(Property prop, AnnotatedElement ae) } + @Override public Object getValue(Object pojo, String name) { try { @@ -270,10 +270,7 @@ public Object getValue(Object pojo, String name) { } } - public void putValue(Object pojo, String name, Object value) { - putValue(pojo, name, value, false); - } - + @Override public void putValue(Object pojo, String name, Object value, boolean ignoreIfMissing) { Property prop = propertyMap.get(name); @@ -299,7 +296,7 @@ public void putValue(Object pojo, String name, Object value, boolean ignoreIfMis if (prop.writeMethod != null) { try { if (value instanceof BigInteger && prop.writeMethod.getParameterCount() >= 1) { - Class type = prop.writeMethod.getParameterTypes()[0]; + Class type = prop.writeMethod.getParameterTypes()[0]; if (type.equals(Long.TYPE) || type.equals(Long.class)) { value = ((BigInteger) value).longValue(); } @@ -330,10 +327,20 @@ public void putValue(Object pojo, String name, Object value, boolean ignoreIfMis } + @Override + public Property getProperty(String name) { + return propertyMap.get(name); + } + + @Override + public String[] getGeneratedColumnNames() { + return generatedColumnNames; + } + /** * Convert a string to an enum const of the appropriate class. */ - private > Object getEnumConst(Class enumType, EnumType type, Object value) { + public static > Object getEnumConst(Class enumType, EnumType type, Object value) { String str = value.toString(); if (type == EnumType.ORDINAL) { Integer ordinalValue = (Integer) value; @@ -342,24 +349,13 @@ private > Object getEnumConst(Class enumType, EnumType type "Invalid ordinal number " + ordinalValue + " for enum class " + enumType.getCanonicalName()); } return enumType.getEnumConstants()[ordinalValue]; - } else { - for (T e : enumType.getEnumConstants()) { - if (str.equals(e.toString())) { - return e; - } + } + + for (T e : enumType.getEnumConstants()) { + if (str.equals(e.toString())) { + return e; } - throw new DbException("Enum value does not exist. value:" + str); } + throw new DbException("Enum value does not exist. value:" + str); } - - @Override - public Property getProperty(String name) { - return propertyMap.get(name); - } - - @Override - public String[] getGeneratedColumnNames() { - return this.generatedColumnNames; - } - } diff --git a/src/main/java/com/dieselpoint/norm/sqlmakers/StandardSqlMaker.java b/src/main/java/com/dieselpoint/norm/sqlmakers/StandardSqlMaker.java index 82f1a96..7553139 100644 --- a/src/main/java/com/dieselpoint/norm/sqlmakers/StandardSqlMaker.java +++ b/src/main/java/com/dieselpoint/norm/sqlmakers/StandardSqlMaker.java @@ -1,336 +1,329 @@ -package com.dieselpoint.norm.sqlmakers; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - -import javax.persistence.Column; - -import com.dieselpoint.norm.DbException; -import com.dieselpoint.norm.Query; -import com.dieselpoint.norm.Util; - -/** - * Produces ANSI-standard SQL. Extend this class to handle different flavors of - * sql. - */ -public class StandardSqlMaker implements SqlMaker { - - private static ConcurrentHashMap, StandardPojoInfo> map = new ConcurrentHashMap<>(); - - public synchronized StandardPojoInfo getPojoInfo(Class rowClass) { - StandardPojoInfo pi = map.get(rowClass); - if (pi == null) { - pi = new StandardPojoInfo(rowClass); - map.put(rowClass, pi); - - makeInsertSql(pi); - makeUpsertSql(pi); - makeUpdateSql(pi); - makeSelectColumns(pi); - } - return pi; - } - - @Override - public String getInsertSql(Query query, Object row) { - StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); - return String.format(pojoInfo.insertSql, Objects.requireNonNullElse(query.getTable(), pojoInfo.table)); - } - - @Override - public Object[] getInsertArgs(Query query, Object row) { - StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); - Object[] args = new Object[pojoInfo.insertSqlArgCount]; - for (int i = 0; i < pojoInfo.insertSqlArgCount; i++) { - args[i] = pojoInfo.getValue(row, pojoInfo.insertColumnNames[i]); - } - return args; - } - - @Override - public String getUpdateSql(Query query, Object row) { - StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); - if (pojoInfo.primaryKeyNames.size() == 0) { - throw new DbException("No primary keys specified in the row. Use the @Id annotation."); - } - return String.format(pojoInfo.updateSql, Objects.requireNonNullElse(query.getTable(), pojoInfo.table)); - } - - @Override - public Object[] getUpdateArgs(Query query, Object row) { - StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); - - int numKeys = pojoInfo.primaryKeyNames.size(); - - Object[] args = new Object[pojoInfo.updateSqlArgCount]; - for (int i = 0; i < pojoInfo.updateSqlArgCount - numKeys; i++) { - args[i] = pojoInfo.getValue(row, pojoInfo.updateColumnNames[i]); - } - // add the value for the where clause to the end - for (int i = 0; i < numKeys; i++) { - Object pk = pojoInfo.getValue(row, pojoInfo.primaryKeyNames.get(i)); - args[pojoInfo.updateSqlArgCount - (numKeys - i)] = pk; - } - return args; - } - - public void makeUpdateSql(StandardPojoInfo pojoInfo) { - - ArrayList cols = new ArrayList<>(); - for (Property prop : pojoInfo.propertyMap.values()) { - - if (prop.isPrimaryKey) { - continue; - } - - if (prop.isGenerated) { - continue; - } - - cols.add(prop.name); - } - pojoInfo.updateColumnNames = cols.toArray(new String[cols.size()]); - pojoInfo.updateSqlArgCount = pojoInfo.updateColumnNames.length + pojoInfo.primaryKeyNames.size(); // + # of - // primary - // keys for - // the where - // arg - - StringBuilder buf = new StringBuilder(); - buf.append("update %s set "); - - for (int i = 0; i < cols.size(); i++) { - if (i > 0) { - buf.append(','); - } - buf.append(cols.get(i)).append("=?"); - } - buf.append(" where "); - - for (int i = 0; i < pojoInfo.primaryKeyNames.size(); i++) { - if (i > 0) { - buf.append(" and "); - } - buf.append(pojoInfo.primaryKeyNames.get(i)).append("=?"); - } - - pojoInfo.updateSql = buf.toString(); - } - - public void makeInsertSql(StandardPojoInfo pojoInfo) { - ArrayList cols = new ArrayList<>(); - for (Property prop : pojoInfo.propertyMap.values()) { - if (prop.isGenerated) { - continue; - } - cols.add(prop.name); - } - pojoInfo.insertColumnNames = cols.toArray(new String[cols.size()]); - pojoInfo.insertSqlArgCount = pojoInfo.insertColumnNames.length; - - pojoInfo.insertSql = "insert into %s (" + Util.join(pojoInfo.insertColumnNames) + // comma sep list? - ") values (" + Util.getQuestionMarks(pojoInfo.insertSqlArgCount) + ")"; - } - - public void makeUpsertSql(StandardPojoInfo pojoInfo) { - } - - private void makeSelectColumns(StandardPojoInfo pojoInfo) { - if (pojoInfo.propertyMap.isEmpty()) { - // this applies if the rowClass is a Map - pojoInfo.selectColumns = "*"; - } else { - ArrayList cols = new ArrayList<>(); - for (Property prop : pojoInfo.propertyMap.values()) { - cols.add(prop.name); - } - pojoInfo.selectColumns = Util.join(cols); - } - } - - @Override - public String getSelectSql(Query query, Class rowClass) { - - // unlike insert and update, this needs to be done dynamically - // and can't be precalculated because of the where and order by - - StandardPojoInfo pojoInfo = getPojoInfo(rowClass); - String columns = pojoInfo.selectColumns; - - String where = query.getWhere(); - String table = query.getTable(); - if (table == null) { - table = pojoInfo.table; - } - String orderBy = query.getOrderBy(); - - StringBuilder out = new StringBuilder(); - out.append("select "); - out.append(columns); - out.append(" from "); - out.append(table); - if (where != null) { - out.append(" where "); - out.append(where); - } - if (orderBy != null) { - out.append(" order by "); - out.append(orderBy); - } - return out.toString(); - } - - @Override - public String getCreateTableSql(Class clazz) { - - StringBuilder buf = new StringBuilder(); - - StandardPojoInfo pojoInfo = getPojoInfo(clazz); - buf.append("create table "); - buf.append(pojoInfo.table); - buf.append(" ("); - - boolean needsComma = false; - for (Property prop : pojoInfo.propertyMap.values()) { - - if (needsComma) { - buf.append(','); - } - needsComma = true; - - Column columnAnnot = prop.columnAnnotation; - if (columnAnnot == null) { - - buf.append(prop.name); - buf.append(" "); - buf.append(getColType(prop.dataType, 255, 10, 2)); - if (prop.isGenerated) { - buf.append(" auto_increment"); - } - - } else { - if (columnAnnot.columnDefinition() != null) { - - // let the column def override everything - buf.append(columnAnnot.columnDefinition()); - - } else { - - buf.append(prop.name); - buf.append(" "); - buf.append(getColType(prop.dataType, columnAnnot.length(), columnAnnot.precision(), - columnAnnot.scale())); - if (prop.isGenerated) { - buf.append(" auto_increment"); - } - - if (columnAnnot.unique()) { - buf.append(" unique"); - } - - if (!columnAnnot.nullable()) { - buf.append(" not null"); - } - } - } - } - - if (pojoInfo.primaryKeyNames.size() > 0) { - buf.append(", primary key ("); - for (int i = 0; i < pojoInfo.primaryKeyNames.size(); i++) { - if (i > 0) { - buf.append(","); - } - buf.append(pojoInfo.primaryKeyNames.get(i)); - } - buf.append(")"); - } - - buf.append(")"); - - return buf.toString(); - } - - protected String getColType(Class dataType, int length, int precision, int scale) { - String colType; - - if (dataType.equals(Integer.class) || dataType.equals(int.class)) { - colType = "integer"; - - } else if (dataType.equals(Long.class) || dataType.equals(long.class)) { - colType = "bigint"; - - } else if (dataType.equals(Double.class) || dataType.equals(double.class)) { - colType = "double"; - - } else if (dataType.equals(Float.class) || dataType.equals(float.class)) { - colType = "float"; - - } else if (dataType.equals(BigDecimal.class)) { - colType = "decimal(" + precision + "," + scale + ")"; - - } else if (dataType.equals(java.util.Date.class)) { - colType = "datetime"; - - } else { - colType = "varchar(" + length + ")"; - } - return colType; - } - - public Object convertValue(Object value, String columnTypeName) { - return value; - } - - @Override - public String getDeleteSql(Query query, Object row) { - - StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); - - String table = query.getTable(); - if (table == null) { - table = pojoInfo.table; - if (table == null) { - throw new DbException("You must specify a table name"); - } - } - - StringBuilder builder = new StringBuilder("delete from "); - builder.append(table).append(" where "); - for (int i = 0; i < pojoInfo.primaryKeyNames.size(); i++) { - if (i > 0) { - builder.append(" and "); - } - builder.append(pojoInfo.primaryKeyNames.get(i)).append("=?"); - } - - return builder.toString(); - } - - @Override - public Object[] getDeleteArgs(Query query, Object row) { - StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); - Object[] args = new Object[pojoInfo.primaryKeyNames.size()]; - - for (int i = 0; i < pojoInfo.primaryKeyNames.size(); i++) { - Object primaryKeyValue = pojoInfo.getValue(row, pojoInfo.primaryKeyNames.get(i)); - args[i] = primaryKeyValue; - } - return args; - } - - @Override - public String getUpsertSql(Query query, Object row) { - String msg = "There's no standard upsert implemention. There is one in the MySql driver, though," - + "so if you're using MySql, call Database.setSqlMaker(new MySqlMaker()); Or roll your own."; - throw new UnsupportedOperationException(msg); - } - - @Override - public Object[] getUpsertArgs(Query query, Object row) { - throw new UnsupportedOperationException(); - } - +package com.dieselpoint.norm.sqlmakers; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +import javax.persistence.Column; + +import com.dieselpoint.norm.DbException; +import com.dieselpoint.norm.Query; +import com.dieselpoint.norm.Util; + +/** + * Produces ANSI-standard SQL. Extend this class to handle different flavors of + * sql. + */ +public class StandardSqlMaker implements SqlMaker { + + protected static final ConcurrentHashMap, StandardPojoInfo> CACHE = new ConcurrentHashMap<>(); + + @Override + public synchronized StandardPojoInfo getPojoInfo(Class rowClass) { + StandardPojoInfo pi = CACHE.get(rowClass); + if (pi == null) { + pi = new StandardPojoInfo(rowClass); + CACHE.put(rowClass, pi); + + makeInsertSql(pi); + makeUpsertSql(pi); + makeUpdateSql(pi); + makeSelectColumns(pi); + } + return pi; + } + + @Override + public String getInsertSql(Query query, Object row) { + StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); + return String.format(pojoInfo.insertSql, Objects.requireNonNullElse(query.getTable(), pojoInfo.table)); + } + + @Override + public Object[] getInsertArgs(Query query, Object row) { + StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); + Object[] args = new Object[pojoInfo.insertSqlArgCount]; + for (int i = 0; i < pojoInfo.insertSqlArgCount; i++) { + args[i] = pojoInfo.getValue(row, pojoInfo.insertColumnNames[i]); + } + return args; + } + + @Override + public String getUpdateSql(Query query, Object row) { + StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); + if (pojoInfo.primaryKeyNames.size() == 0) { + throw new DbException("No primary keys specified in the row. Use the @Id annotation."); + } + return String.format(pojoInfo.updateSql, Objects.requireNonNullElse(query.getTable(), pojoInfo.table)); + } + + @Override + public Object[] getUpdateArgs(Query query, Object row) { + StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); + + int numKeys = pojoInfo.primaryKeyNames.size(); + + Object[] args = new Object[pojoInfo.updateSqlArgCount]; + for (int i = 0; i < pojoInfo.updateSqlArgCount - numKeys; i++) { + args[i] = pojoInfo.getValue(row, pojoInfo.updateColumnNames[i]); + } + // add the value for the where clause to the end + for (int i = 0; i < numKeys; i++) { + Object pk = pojoInfo.getValue(row, pojoInfo.primaryKeyNames.get(i)); + args[pojoInfo.updateSqlArgCount - (numKeys - i)] = pk; + } + return args; + } + + public void makeUpdateSql(StandardPojoInfo pojoInfo) { + + ArrayList cols = new ArrayList<>(); + for (Property prop : pojoInfo.propertyMap.values()) { + + if (prop.isPrimaryKey) { + continue; + } + + if (prop.isGenerated) { + continue; + } + + cols.add(prop.name); + } + pojoInfo.updateColumnNames = cols.toArray(new String[cols.size()]); + pojoInfo.updateSqlArgCount = pojoInfo.updateColumnNames.length + pojoInfo.primaryKeyNames.size(); // + # of + // primary + // keys for + // the where + // arg + + StringBuilder buf = new StringBuilder(); + buf.append("update %s set "); + + for (int i = 0, size = cols.size(); i < size; i++) { + if (i > 0) { + buf.append(','); + } + buf.append(cols.get(i)).append("=?"); + } + buf.append(" where "); + + for (int i = 0, size = pojoInfo.primaryKeyNames.size(); i < size; i++) { + if (i > 0) { + buf.append(" and "); + } + buf.append(pojoInfo.primaryKeyNames.get(i)).append("=?"); + } + + pojoInfo.updateSql = buf.toString(); + } + + public void makeInsertSql(StandardPojoInfo pojoInfo) { + ArrayList cols = new ArrayList<>(); + for (Property prop : pojoInfo.propertyMap.values()) { + if (prop.isGenerated) { + continue; + } + cols.add(prop.name); + } + pojoInfo.insertColumnNames = cols.toArray(new String[cols.size()]); + pojoInfo.insertSqlArgCount = pojoInfo.insertColumnNames.length; + + pojoInfo.insertSql = "insert into %s (" + Util.join(pojoInfo.insertColumnNames) + // comma sep list? + ") values (" + Util.joinChars('?', pojoInfo.insertSqlArgCount) + ")"; + } + + public void makeUpsertSql(StandardPojoInfo pojoInfo) { + } + + public void makeSelectColumns(StandardPojoInfo pojoInfo) { + if (pojoInfo.propertyMap.isEmpty()) { + // this applies if the rowClass is a Map + pojoInfo.selectColumns = "*"; + } else { + String[] cols = new String[pojoInfo.propertyMap.size()]; + int i = 0; + for (Property prop : pojoInfo.propertyMap.values()) + cols[i++] = prop.name; + pojoInfo.selectColumns = Util.join(cols); + } + } + + @Override + public String getSelectSql(Query query, Class rowClass) { + + // unlike insert and update, this needs to be done dynamically + // and can't be precalculated because of the where and order by + + StandardPojoInfo pojoInfo = getPojoInfo(rowClass); + String columns = pojoInfo.selectColumns; + + String where = query.getWhere(); + String table = query.getTable(); + if (table == null) { + table = pojoInfo.table; + } + String orderBy = query.getOrderBy(); + + StringBuilder out = new StringBuilder(); + out.append("select "); + out.append(columns); + out.append(" from "); + out.append(table); + if (where != null) { + out.append(" where "); + out.append(where); + } + if (orderBy != null) { + out.append(" order by "); + out.append(orderBy); + } + return out.toString(); + } + + @Override + public String getCreateTableSql(Class clazz) { + + StringBuilder buf = new StringBuilder(); + + StandardPojoInfo pojoInfo = getPojoInfo(clazz); + buf.append("create table "); + buf.append(pojoInfo.table); + buf.append(" ("); + + boolean needsComma = false; + for (Property prop : pojoInfo.propertyMap.values()) { + + if (needsComma) { + buf.append(','); + } + needsComma = true; + + Column columnAnnot = prop.columnAnnotation; + if (columnAnnot == null) { + + buf.append(prop.name); + buf.append(" "); + buf.append(getColType(prop.dataType, 255, 10, 2)); + if (prop.isGenerated) { + buf.append(" auto_increment"); + } + + } else if (columnAnnot.columnDefinition() != null) { + + // let the column def override everything + buf.append(columnAnnot.columnDefinition()); + + } else { + + buf.append(prop.name); + buf.append(" "); + buf.append(getColType(prop.dataType, columnAnnot.length(), columnAnnot.precision(), + columnAnnot.scale())); + if (prop.isGenerated) { + buf.append(" auto_increment"); + } + + if (columnAnnot.unique()) { + buf.append(" unique"); + } + + if (!columnAnnot.nullable()) { + buf.append(" not null"); + } + } + } + + if (pojoInfo.primaryKeyNames.size() > 0) { + buf.append(", primary key (").append(Util.join(pojoInfo.primaryKeyNames)).append(")"); + } + + buf.append(")"); + + return buf.toString(); + } + + protected String getColType(Class dataType, int length, int precision, int scale) { + String colType; + + if (dataType.equals(Integer.class) || dataType.equals(int.class)) { + colType = "integer"; + + } else if (dataType.equals(Long.class) || dataType.equals(long.class)) { + colType = "bigint"; + + } else if (dataType.equals(Double.class) || dataType.equals(double.class)) { + colType = "double"; + + } else if (dataType.equals(Float.class) || dataType.equals(float.class)) { + colType = "float"; + + } else if (dataType.equals(BigDecimal.class)) { + colType = "decimal(" + precision + "," + scale + ")"; + + } else if (dataType.equals(java.util.Date.class)) { + colType = "datetime"; + + } else { + colType = "varchar(" + length + ")"; + } + return colType; + } + + @Override + public Object convertValue(Object value, String columnTypeName) { + return value; + } + + @Override + public String getDeleteSql(Query query, Object row) { + + StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); + + String table = query.getTable(); + if (table == null) { + table = pojoInfo.table; + if (table == null) { + throw new DbException("You must specify a table name"); + } + } + + StringBuilder builder = new StringBuilder("delete from "); + builder.append(table).append(" where "); + for (int i = 0, size = pojoInfo.primaryKeyNames.size(); i < size; i++) { + if (i > 0) { + builder.append(" and "); + } + builder.append(pojoInfo.primaryKeyNames.get(i)).append("=?"); + } + + return builder.toString(); + } + + @Override + public Object[] getDeleteArgs(Query query, Object row) { + StandardPojoInfo pojoInfo = getPojoInfo(row.getClass()); + Object[] args = new Object[pojoInfo.primaryKeyNames.size()]; + + for (int i = 0, size = pojoInfo.primaryKeyNames.size(); i < size; i++) { + Object primaryKeyValue = pojoInfo.getValue(row, pojoInfo.primaryKeyNames.get(i)); + args[i] = primaryKeyValue; + } + return args; + } + + @Override + public String getUpsertSql(Query query, Object row) { + String msg = "There's no standard upsert implemention. There is one in the MySql driver, though," + + "so if you're using MySql, call Database.setSqlMaker(new MySqlMaker()); Or roll your own."; + throw new UnsupportedOperationException(msg); + } + + @Override + public Object[] getUpsertArgs(Query query, Object row) { + throw new UnsupportedOperationException(); + } + } \ No newline at end of file diff --git a/src/test/java/com/dieselpoint/norm/SampleCode.java b/src/test/java/com/dieselpoint/norm/SampleCode.java index 6bb97d5..9ce5c63 100644 --- a/src/test/java/com/dieselpoint/norm/SampleCode.java +++ b/src/test/java/com/dieselpoint/norm/SampleCode.java @@ -1,138 +1,139 @@ -package com.dieselpoint.norm; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.sql.SQLException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.persistence.Column; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Transient; - -public class SampleCode { - - static public void main(String[] args) throws SQLException, FileNotFoundException, IOException { - - Setup.setSysProperties(); - - Database db = new Database(); - - // db.setSqlMaker(new PostgresMaker()); // set this to match your sql flavor - - /* test straight sql */ - db.sql("drop table if exists names").execute(); - - /* test create table */ - db.createTable(Name.class); - - /* test inserts */ - Name john = new Name("John", "Doe"); - db.insert(john); - - Name bill = new Name("Bill", "Smith"); - db.insert(bill); - - /* test where clause, also id and generated values */ - List list = db.where("firstname=?", "John").results(Name.class); - dump("john only:", list); - - /* test delete single record */ - db.delete(john); - List list1 = db.orderBy("lastname").results(Name.class); - dump("bill only:", list1); - - /* test update single record */ - bill.firstname = "Joe"; - int rowsAffected = db.update(bill).getRowsAffected(); - List list2 = db.results(Name.class); - dump("bill is now joe, and rowsAffected=" + rowsAffected, list2); - - /* test using a map for results instead of a pojo */ - Map map = db.sql("select count(*) as count from names").first(HashMap.class); - System.out.println("Num records (should be 1):" + map.get("count")); - - /* test using a primitive for results instead of a pojo */ - Long count = db.sql("select count(*) as count from names").first(Long.class); - System.out.println("Num records (should be 1):" + count); - - /* test delete with where clause */ - db.table("names").where("firstname=?", "Joe").delete(); - - /* make sure the delete happened */ - count = db.sql("select count(*) as count from names").first(Long.class); - System.out.println("Num records (should be 0):" + count); - - /* test transactions */ - db.insert(new Name("Fred", "Jones")); - Transaction trans = db.startTransaction(); - db.transaction(trans).insert(new Name("Sam", "Williams")); - db.transaction(trans).insert(new Name("George ", "Johnson")); - trans.rollback(); - List list3 = db.results(Name.class); - dump("fred only:", list3); - - // db.sql("drop table names").execute(); - } - - public static void dump(String label, List list) { - System.out.println(label); - for (Name n : list) { - System.out.println(n.toString()); - } - } - - @Table(name = "names") - static public class Name { - - /* - * Strongly recommend that you make all your column names lower-case, both in - * the pojo and in the database. Many SQL databases handle mixed-case names - * incorrectly. - */ - - // must have 0-arg constructor - public Name() { - } - - // can also have convenience constructor - public Name(String firstname, String lastname) { - this.firstname = firstname; - this.lastname = lastname; - } - - // primary key, generated on the server side - @Id - @GeneratedValue - public long id; - - // a public property without getter or setter - public String firstname; - - // a private property with getter and setter below - private String lastname; - - @Column(name = "lastname") // must do this for Postgres - public String getLastName() { - return lastname; - } - - public void setLastName(String lastname) { - this.lastname = lastname; - } - - @Transient - public String ignoreMe; - - // ignore static fields - public static String ignoreThisToo; - - public String toString() { - return id + " " + firstname + " " + lastname; - } - } - -} +package com.dieselpoint.norm; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.persistence.Column; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Transient; + +public class SampleCode { + + static public void main(String[] args) throws SQLException, FileNotFoundException, IOException { + + Setup.setSysProperties(); + + Database db = new Database(); + + // db.setSqlMaker(new PostgresMaker()); // set this to match your sql flavor + + /* test straight sql */ + db.sql("drop table if exists names").execute(); + + /* test create table */ + db.createTable(Name.class); + + /* test inserts */ + Name john = new Name("John", "Doe"); + db.insert(john); + + Name bill = new Name("Bill", "Smith"); + db.insert(bill); + + /* test where clause, also id and generated values */ + List list = db.where("firstname=?", "John").results(Name.class); + dump("john only:", list); + + /* test delete single record */ + db.delete(john); + List list1 = db.orderBy("lastname").results(Name.class); + dump("bill only:", list1); + + /* test update single record */ + bill.firstname = "Joe"; + int rowsAffected = db.update(bill).getRowsAffected(); + List list2 = db.results(Name.class); + dump("bill is now joe, and rowsAffected=" + rowsAffected, list2); + + /* test using a map for results instead of a pojo */ + Map map = db.sql("select count(*) as count from names").first(HashMap.class); + System.out.println("Num records (should be 1):" + map.get("count")); + + /* test using a primitive for results instead of a pojo */ + Long count = db.sql("select count(*) as count from names").first(Long.class); + System.out.println("Num records (should be 1):" + count); + + /* test delete with where clause */ + db.table("names").where("firstname=?", "Joe").delete(); + + /* make sure the delete happened */ + count = db.sql("select count(*) as count from names").first(Long.class); + System.out.println("Num records (should be 0):" + count); + + /* test transactions */ + db.insert(new Name("Fred", "Jones")); + Transaction trans = db.startTransaction(); + db.transaction(trans).insert(new Name("Sam", "Williams")); + db.transaction(trans).insert(new Name("George ", "Johnson")); + trans.rollback(); + List list3 = db.results(Name.class); + dump("fred only:", list3); + + // db.sql("drop table names").execute(); + } + + public static void dump(String label, List list) { + System.out.println(label); + for (Name n : list) { + System.out.println(n.toString()); + } + } + + @Table(name = "names") + static public class Name { + + /* + * Strongly recommend that you make all your column names lower-case, both in + * the pojo and in the database. Many SQL databases handle mixed-case names + * incorrectly. + */ + + // must have 0-arg constructor + public Name() { + } + + // can also have convenience constructor + public Name(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + + // primary key, generated on the server side + @Id + @GeneratedValue + public long id; + + // a public property without getter or setter + public String firstname; + + // a private property with getter and setter below + private String lastname; + + @Column(name = "lastname") // must do this for Postgres + public String getLastName() { + return lastname; + } + + public void setLastName(String lastname) { + this.lastname = lastname; + } + + @Transient + public String ignoreMe; + + // ignore static fields + public static String ignoreThisToo; + + @Override + public String toString() { + return id + " " + firstname + " " + lastname; + } + } + +} diff --git a/src/test/java/com/dieselpoint/norm/Setup.java b/src/test/java/com/dieselpoint/norm/Setup.java index db9fdd6..63a78ae 100644 --- a/src/test/java/com/dieselpoint/norm/Setup.java +++ b/src/test/java/com/dieselpoint/norm/Setup.java @@ -1,51 +1,51 @@ -package com.dieselpoint.norm; - -public class Setup { - - public static void setSysProperties() { - - /*- This is broken, per the Hikari docs. Must use jdbcUrl method instead. - System.setProperty("norm.dataSourceClassName", "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"); - System.setProperty("norm.serverName", "localhost"); - System.setProperty("norm.databaseName", "mydb"); - */ - - /*- - * MySQL JDBC incorrectly reports bigint SQL datatype as a BigInteger. - * https://stackoverflow.com/questions/65078455/mysql-jdbc-driver-incorrectly-reports-bigint-as-biginteger - * / - System.setProperty("norm.jdbcUrl", - "jdbc:mysql://localhost:3306/mydb?useSSL=false&allowPublicKeyRetrieval=true"); - System.setProperty("norm.user", "root"); - // System.setProperty("norm.password", "rootpassword"); - */ - - /*- - System.setProperty("norm.dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource"); - System.setProperty("norm.user", "postgres"); - System.setProperty("norm.password", "postgres"); - */ - - /*- - * H2 handles mixed-case column names incorrectly. - */ - System.setProperty("norm.jdbcUrl", "jdbc:h2:./h2test;database_to_upper=false"); - System.setProperty("norm.user", "root"); - System.setProperty("norm.password", "rootpassword"); - - /*- - * SampleCode doesn't yet work because the sqlite create table syntax is different. Need a new SQL maker. - * / - System.setProperty("norm.jdbcUrl", "jdbc:sqlite:sqlitetest.db"); - System.setProperty("norm.user", "root"); - System.setProperty("norm.password", "rootpassword"); - */ - - /*- - * Does not run sample code because the "drop database if exists names" chokes on "exists" - System.setProperty("norm.jdbcUrl", "jdbc:derby:mydb;create=true"); - System.setProperty("norm.user", "root"); - */ - - } -} +package com.dieselpoint.norm; + +public class Setup { + + public static void setSysProperties() { + + /*- This is broken, per the Hikari docs. Must use jdbcUrl method instead. + System.setProperty("norm.dataSourceClassName", "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"); + System.setProperty("norm.serverName", "localhost"); + System.setProperty("norm.databaseName", "mydb"); + */ + + /*- + * MySQL JDBC incorrectly reports bigint SQL datatype as a BigInteger. + * https://stackoverflow.com/questions/65078455/mysql-jdbc-driver-incorrectly-reports-bigint-as-biginteger + * / + System.setProperty("norm.jdbcUrl", + "jdbc:mysql://localhost:3306/mydb?useSSL=false&allowPublicKeyRetrieval=true"); + System.setProperty("norm.user", "root"); + // System.setProperty("norm.password", "rootpassword"); + */ + + /*- + System.setProperty("norm.dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource"); + System.setProperty("norm.user", "postgres"); + System.setProperty("norm.password", "postgres"); + */ + + /*- + * H2 handles mixed-case column names incorrectly. + */ + System.setProperty("norm.jdbcUrl", "jdbc:h2:./h2test;database_to_upper=false"); + System.setProperty("norm.user", "root"); + System.setProperty("norm.password", "rootpassword"); + + /*- + * SampleCode doesn't yet work because the sqlite create table syntax is different. Need a new SQL maker. + * / + System.setProperty("norm.jdbcUrl", "jdbc:sqlite:sqlitetest.db"); + System.setProperty("norm.user", "root"); + System.setProperty("norm.password", "rootpassword"); + */ + + /*- + * Does not run sample code because the "drop database if exists names" chokes on "exists" + System.setProperty("norm.jdbcUrl", "jdbc:derby:mydb;create=true"); + System.setProperty("norm.user", "root"); + */ + + } +} diff --git a/src/test/java/com/dieselpoint/norm/TestCompositeKey.java b/src/test/java/com/dieselpoint/norm/TestCompositeKey.java index 6fc6746..f23391f 100644 --- a/src/test/java/com/dieselpoint/norm/TestCompositeKey.java +++ b/src/test/java/com/dieselpoint/norm/TestCompositeKey.java @@ -1,18 +1,18 @@ package com.dieselpoint.norm; -import com.dieselpoint.norm.sqlmakers.MySqlMaker; -import org.junit.Before; -import org.junit.Test; +import static org.junit.Assert.assertEquals; -import javax.persistence.Id; -import javax.persistence.Table; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.junit.Before; +import org.junit.Test; + +import com.dieselpoint.norm.sqlmakers.MySqlMaker; public class TestCompositeKey { @@ -145,6 +145,7 @@ public static class Row { public String otherId; public String name; + @Override public String toString() { return id + name; } diff --git a/src/test/java/com/dieselpoint/norm/TestMySqlUpsert.java b/src/test/java/com/dieselpoint/norm/TestMySqlUpsert.java index 8ae1c65..f31f852 100644 --- a/src/test/java/com/dieselpoint/norm/TestMySqlUpsert.java +++ b/src/test/java/com/dieselpoint/norm/TestMySqlUpsert.java @@ -1,52 +1,52 @@ -package com.dieselpoint.norm; - -import static org.junit.Assert.fail; - -import java.util.HashMap; -import java.util.List; - -import javax.persistence.Column; -import javax.persistence.Table; - -import org.junit.Test; - -import com.dieselpoint.norm.sqlmakers.MySqlMaker; - -public class TestMySqlUpsert { - - @Test - public void test() { - - Setup.setSysProperties(); - - Database db = new Database(); - db.setSqlMaker(new MySqlMaker()); - - db.sql("drop table if exists upserttest").execute(); - - db.createTable(Row.class); - - Row row = new Row(); - row.id = 1; - row.name = "bob"; - db.upsert(row); - - row.name = "Fred"; - db.upsert(row); - - List list = db.table("upserttest").results(HashMap.class); - - String listStr = list.toString(); - if (!listStr.equals("[{name=Fred, id=1}]")) { - fail(); - } - } - - @Table(name="upserttest") - public static class Row { - @Column(unique=true) - public long id; - public String name; - } - -} +package com.dieselpoint.norm; + +import static org.junit.Assert.fail; + +import java.util.HashMap; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.Table; + +import org.junit.Test; + +import com.dieselpoint.norm.sqlmakers.MySqlMaker; + +public class TestMySqlUpsert { + + @Test + public void test() { + + Setup.setSysProperties(); + + Database db = new Database(); + db.setSqlMaker(new MySqlMaker()); + + db.sql("drop table if exists upserttest").execute(); + + db.createTable(Row.class); + + Row row = new Row(); + row.id = 1; + row.name = "bob"; + db.upsert(row); + + row.name = "Fred"; + db.upsert(row); + + List list = db.table("upserttest").results(HashMap.class); + + String listStr = list.toString(); + if (!listStr.equals("[{name=Fred, id=1}]")) { + fail(); + } + } + + @Table(name="upserttest") + public static class Row { + @Column(unique=true) + public long id; + public String name; + } + +} diff --git a/src/test/java/com/dieselpoint/norm/TestSelect.java b/src/test/java/com/dieselpoint/norm/TestSelect.java index 0962296..c2334ba 100644 --- a/src/test/java/com/dieselpoint/norm/TestSelect.java +++ b/src/test/java/com/dieselpoint/norm/TestSelect.java @@ -1,63 +1,64 @@ -package com.dieselpoint.norm; - -import static org.junit.Assert.fail; - -import java.util.LinkedHashMap; -import java.util.Map; - -import javax.persistence.Column; -import javax.persistence.Table; - -import org.junit.Test; - -public class TestSelect { - - @Test - public void test() { - - Setup.setSysProperties(); - - Database db = new Database(); - - db.sql("drop table if exists selecttest").execute(); - - db.createTable(Row.class); - - Row row = new Row(); - row.id = 99; - row.name = "bob"; - db.insert(row); - - // primitive - Long myId = db.sql("select id from selecttest").first(Long.class); - if (myId != 99) { - fail(); - } - - // map - Map myMap = db.table("selecttest").first(LinkedHashMap.class); - String str = myMap.toString(); - if (!str.equals("{id=99, name=bob}")) { - fail(); - } - - // pojo - Row myRow = db.first(Row.class); - String myRowStr = myRow.toString(); - if (!myRowStr.equals("99bob")) { - fail(); - } - - } - - @Table(name="selecttest") - public static class Row { - @Column(unique=true) - public long id; - public String name; - public String toString() { - return id + name; - } - } - -} +package com.dieselpoint.norm; + +import static org.junit.Assert.fail; + +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.persistence.Column; +import javax.persistence.Table; + +import org.junit.Test; + +public class TestSelect { + + @Test + public void test() { + + Setup.setSysProperties(); + + Database db = new Database(); + + db.sql("drop table if exists selecttest").execute(); + + db.createTable(Row.class); + + Row row = new Row(); + row.id = 99; + row.name = "bob"; + db.insert(row); + + // primitive + Long myId = db.sql("select id from selecttest").first(Long.class); + if (myId != 99) { + fail(); + } + + // map + Map myMap = db.table("selecttest").first(LinkedHashMap.class); + String str = myMap.toString(); + if (!str.equals("{id=99, name=bob}")) { + fail(); + } + + // pojo + Row myRow = db.first(Row.class); + String myRowStr = myRow.toString(); + if (!myRowStr.equals("99bob")) { + fail(); + } + + } + + @Table(name="selecttest") + public static class Row { + @Column(unique=true) + public long id; + public String name; + @Override + public String toString() { + return id + name; + } + } + +} diff --git a/src/test/java/com/dieselpoint/norm/TestSerialize.java b/src/test/java/com/dieselpoint/norm/TestSerialize.java index 280ca93..b052168 100644 --- a/src/test/java/com/dieselpoint/norm/TestSerialize.java +++ b/src/test/java/com/dieselpoint/norm/TestSerialize.java @@ -1,36 +1,36 @@ -package com.dieselpoint.norm; - -import java.util.List; - -import org.junit.Test; - -import com.dieselpoint.norm.serialize.DbSerializable; -import com.dieselpoint.norm.serialize.DbSerializer; - -public class TestSerialize { - - @Test - public void test() { - - } - - class MyPojo { - @DbSerializer(MySerializer.class) - public List myList; - } - - class MySerializer implements DbSerializable { - - @Override - public String serialize(Object in) { - return in.toString(); - } - - @Override - public Object deserialize(String in, Class targetClass) { - Object out = null; // convert the string back to a list here - return out; - } - } - -} +package com.dieselpoint.norm; + +import java.util.List; + +import org.junit.Test; + +import com.dieselpoint.norm.serialize.DbSerializable; +import com.dieselpoint.norm.serialize.DbSerializer; + +public class TestSerialize { + + @Test + public void test() { + + } + + class MyPojo { + @DbSerializer(MySerializer.class) + public List myList; + } + + class MySerializer implements DbSerializable { + + @Override + public String serialize(Object in) { + return in.toString(); + } + + @Override + public Object deserialize(String in, Class targetClass) { + Object out = null; // convert the string back to a list here + return out; + } + } + +} diff --git a/src/test/java/com/dieselpoint/norm/sqlmakers/MySqlMakerTest.java b/src/test/java/com/dieselpoint/norm/sqlmakers/MySqlMakerTest.java index 2955ee2..edaf715 100644 --- a/src/test/java/com/dieselpoint/norm/sqlmakers/MySqlMakerTest.java +++ b/src/test/java/com/dieselpoint/norm/sqlmakers/MySqlMakerTest.java @@ -12,53 +12,53 @@ import com.dieselpoint.norm.Query; public class MySqlMakerTest { - MySqlMaker sut; - Database db; + MySqlMaker sut; + Database db; - @Before - public void setup() { - sut = new MySqlMaker(); - db = mock(Database.class); + @Before + public void setup() { + sut = new MySqlMaker(); + db = mock(Database.class); - when(db.getSqlMaker()).thenReturn(sut); - } + when(db.getSqlMaker()).thenReturn(sut); + } - @Test - public void getUpsertSql() { - Query query = new Query(db); + @Test + public void getUpsertSql() { + Query query = new Query(db); - StandardSqlMakerTest.TestTable testTable = new StandardSqlMakerTest.TestTable(); - testTable.setId(2); - testTable.setName("test"); + StandardSqlMakerTest.TestTable testTable = new StandardSqlMakerTest.TestTable(); + testTable.setId(2); + testTable.setName("test"); - String updateSql = sut.getUpsertSql(query, testTable); + String updateSql = sut.getUpsertSql(query, testTable); - assertEquals(updateSql, "insert into testTable (name) values (?) on duplicate key update name=?"); - } + assertEquals(updateSql, "insert into testTable (name) values (?) on duplicate key update name=?"); + } - @Test - public void getUpsertArgs() { - Query query = new Query(db); + @Test + public void getUpsertArgs() { + Query query = new Query(db); - StandardSqlMakerTest.TestTable testTable = new StandardSqlMakerTest.TestTable(); - testTable.setId(2); - testTable.setName("test"); + StandardSqlMakerTest.TestTable testTable = new StandardSqlMakerTest.TestTable(); + testTable.setId(2); + testTable.setName("test"); - Object[] upsertArgs = sut.getUpsertArgs(query, testTable); + Object[] upsertArgs = sut.getUpsertArgs(query, testTable); - assertEquals(upsertArgs.length, 2); - assertArrayEquals(upsertArgs, new Object[] { "test", "test" }); - } + assertEquals(upsertArgs.length, 2); + assertArrayEquals(upsertArgs, new Object[] { "test", "test" }); + } - @Test - public void makeUpsertSql() { - StandardSqlMakerTest.TestTable testTable = new StandardSqlMakerTest.TestTable(); - testTable.setId(2); - testTable.setName("test"); + @Test + public void makeUpsertSql() { + StandardSqlMakerTest.TestTable testTable = new StandardSqlMakerTest.TestTable(); + testTable.setId(2); + testTable.setName("test"); - StandardPojoInfo pojoInfo = sut.getPojoInfo(StandardSqlMakerTest.TestTable.class); - sut.makeUpsertSql(pojoInfo); + StandardPojoInfo pojoInfo = sut.getPojoInfo(StandardSqlMakerTest.TestTable.class); + sut.makeUpsertSql(pojoInfo); - assertEquals(pojoInfo.upsertSql, "insert into testTable (name) values (?) on duplicate key update name=?"); - } + assertEquals(pojoInfo.upsertSql, "insert into testTable (name) values (?) on duplicate key update name=?"); + } } \ No newline at end of file diff --git a/src/test/java/com/dieselpoint/norm/sqlmakers/StandardSqlMakerTest.java b/src/test/java/com/dieselpoint/norm/sqlmakers/StandardSqlMakerTest.java index 69a3d57..a5250d3 100644 --- a/src/test/java/com/dieselpoint/norm/sqlmakers/StandardSqlMakerTest.java +++ b/src/test/java/com/dieselpoint/norm/sqlmakers/StandardSqlMakerTest.java @@ -18,139 +18,139 @@ public class StandardSqlMakerTest { - StandardSqlMaker sut; - Database db; + StandardSqlMaker sut; + Database db; - @Before - public void setup() { - sut = new StandardSqlMaker(); - db = mock(Database.class); + @Before + public void setup() { + sut = new StandardSqlMaker(); + db = mock(Database.class); - when(db.getSqlMaker()).thenReturn(sut); - } + when(db.getSqlMaker()).thenReturn(sut); + } - @Test - public void getInsertSql() { - Query query = new Query(db); + @Test + public void getInsertSql() { + Query query = new Query(db); - TestTable testTable = new TestTable(); - testTable.setId(1); - testTable.setName("test"); + TestTable testTable = new TestTable(); + testTable.setId(1); + testTable.setName("test"); - String insertSql = sut.getInsertSql(query, testTable); + String insertSql = sut.getInsertSql(query, testTable); - assertEquals(insertSql, "insert into testTable (name) values (?)"); - } + assertEquals(insertSql, "insert into testTable (name) values (?)"); + } - @Test - public void getUpdateSql() { - Query query = new Query(db); + @Test + public void getUpdateSql() { + Query query = new Query(db); - TestTable testTable = new TestTable(); + TestTable testTable = new TestTable(); - String updateSql = sut.getUpdateSql(query, testTable); + String updateSql = sut.getUpdateSql(query, testTable); - assertEquals(updateSql, "update testTable set name=? where id=?"); - } + assertEquals(updateSql, "update testTable set name=? where id=?"); + } - @Test - public void getSelectSql() { - Query query = new Query(db); + @Test + public void getSelectSql() { + Query query = new Query(db); - String selectSql = sut.getSelectSql(query, TestTable.class); + String selectSql = sut.getSelectSql(query, TestTable.class); - assertEquals(selectSql, "select id,name from testTable"); - } + assertEquals(selectSql, "select id,name from testTable"); + } - @Test - public void getCreateTableSql() { - String createTableSql = sut.getCreateTableSql(TestTable.class); + @Test + public void getCreateTableSql() { + String createTableSql = sut.getCreateTableSql(TestTable.class); - assertEquals(createTableSql, "create table testTable (id integer auto_increment,name varchar(255), primary key (id))"); - } + assertEquals(createTableSql, "create table testTable (id integer auto_increment,name varchar(255), primary key (id))"); + } - @Test - public void getDeleteSql() { - TestTable testTable = new TestTable(); + @Test + public void getDeleteSql() { + TestTable testTable = new TestTable(); - String deleteSql = sut.getDeleteSql(new Query(db), testTable); + String deleteSql = sut.getDeleteSql(new Query(db), testTable); - assertEquals(deleteSql, "delete from testTable where id=?"); - } + assertEquals(deleteSql, "delete from testTable where id=?"); + } - @Test - public void getUpdateArgs() { - Query query = new Query(db); + @Test + public void getUpdateArgs() { + Query query = new Query(db); - TestTable testTable = new TestTable(); - testTable.setId(1); - testTable.setName("test"); + TestTable testTable = new TestTable(); + testTable.setId(1); + testTable.setName("test"); - Object[] updateArgs = sut.getUpdateArgs(query, testTable); + Object[] updateArgs = sut.getUpdateArgs(query, testTable); - assertEquals(updateArgs.length,2); + assertEquals(updateArgs.length,2); - assertArrayEquals(updateArgs, new Object[] {"test", 1}); - } + assertArrayEquals(updateArgs, new Object[] {"test", 1}); + } - @Test - public void getInsertArgs() { - Query query = new Query(db); + @Test + public void getInsertArgs() { + Query query = new Query(db); - TestTable testTable = new TestTable(); - testTable.setId(1); - testTable.setName("test"); + TestTable testTable = new TestTable(); + testTable.setId(1); + testTable.setName("test"); - Object[] insertArgs = sut.getUpdateArgs(query, testTable); + Object[] insertArgs = sut.getUpdateArgs(query, testTable); - assertEquals(insertArgs.length,2); + assertEquals(insertArgs.length,2); - assertArrayEquals(insertArgs, new Object[] {"test", 1}); - } + assertArrayEquals(insertArgs, new Object[] {"test", 1}); + } - @Test - public void getDeleteArgs() { - Query query = new Query(db); + @Test + public void getDeleteArgs() { + Query query = new Query(db); - TestTable testTable = new TestTable(); - testTable.setId(1); - testTable.setName("test"); + TestTable testTable = new TestTable(); + testTable.setId(1); + testTable.setName("test"); - Object[] deleteArgs = sut.getDeleteArgs(query, testTable); + Object[] deleteArgs = sut.getDeleteArgs(query, testTable); - assertEquals(deleteArgs.length,1); + assertEquals(deleteArgs.length,1); - assertArrayEquals(deleteArgs, new Object[] { 1 }); - } + assertArrayEquals(deleteArgs, new Object[] { 1 }); + } - @Test(expected = UnsupportedOperationException.class) - public void getUpsertArgs() { - sut.getUpsertArgs(new Query(db), new TestTable()); - } + @Test(expected = UnsupportedOperationException.class) + public void getUpsertArgs() { + sut.getUpsertArgs(new Query(db), new TestTable()); + } - @Table(name = "testTable") - static class TestTable { - private int id; - private String name; + @Table(name = "testTable") + static class TestTable { + private int id; + private String name; - @Id - @GeneratedValue - @Column(name = "id") - public int getId() { - return id; - } + @Id + @GeneratedValue + @Column(name = "id") + public int getId() { + return id; + } - public void setId(int id) { - this.id = id; - } + public void setId(int id) { + this.id = id; + } - @Column(name = "name") - public String getName() { - return name; - } + @Column(name = "name") + public String getName() { + return name; + } - public void setName(String name) { - this.name = name; - } - } + public void setName(String name) { + this.name = name; + } + } } \ No newline at end of file