diff options
author | James Moger <james.moger@gitblit.com> | 2014-11-10 11:59:09 -0500 |
---|---|---|
committer | James Moger <james.moger@gitblit.com> | 2014-11-10 21:46:37 -0500 |
commit | a6df2de41953e10db1527e54acd734c0f0a1fa28 (patch) | |
tree | ad3a86dc2a54cf3ac9418e0e5e86f396f4e1dfb0 | |
parent | c1d81bcdfc948b417964c6b69be2ee5801e5e1c9 (diff) | |
download | iciql-a6df2de41953e10db1527e54acd734c0f0a1fa28.tar.gz iciql-a6df2de41953e10db1527e54acd734c0f0a1fa28.zip |
Implement DAO externalized statement loading based on runtime Mode
-rw-r--r-- | README.markdown | 2 | ||||
-rw-r--r-- | src/main/java/com/iciql/DaoClasspathStatementProvider.java | 95 | ||||
-rw-r--r-- | src/main/java/com/iciql/DaoProxy.java | 13 | ||||
-rw-r--r-- | src/main/java/com/iciql/DaoStatementProvider.java | 36 | ||||
-rw-r--r-- | src/main/java/com/iciql/Db.java | 33 | ||||
-rw-r--r-- | src/site/dao.mkd | 43 | ||||
-rw-r--r-- | src/site/index.mkd | 8 | ||||
-rw-r--r-- | src/test/java/com/iciql/test/IciqlSuite.java | 10 | ||||
-rw-r--r-- | src/test/java/com/iciql/test/ProductDaoTest.java | 53 | ||||
-rw-r--r-- | src/test/java/iciql.properties | 7 |
10 files changed, 285 insertions, 15 deletions
diff --git a/README.markdown b/README.markdown index fdc5699..62079ce 100644 --- a/README.markdown +++ b/README.markdown @@ -5,7 +5,7 @@ iciql **is**... - a model-based, database access wrapper for JDBC
- for modest database schemas and basic statement generation
- for those who want to write code, instead of SQL, using IDE completion and compile-time type-safety
-- small (225KB with debug symbols) with no runtime dependencies
+- small (<250KB with debug symbols) with no runtime dependencies
- pronounced *icicle* (although it could be French: *ici ql* - here query language)
- a friendly fork of the H2 [JaQu](http://h2database.com/html/jaqu.html) project
diff --git a/src/main/java/com/iciql/DaoClasspathStatementProvider.java b/src/main/java/com/iciql/DaoClasspathStatementProvider.java new file mode 100644 index 0000000..b1bbe14 --- /dev/null +++ b/src/main/java/com/iciql/DaoClasspathStatementProvider.java @@ -0,0 +1,95 @@ +/* + * Copyright 2014 James Moger. + * + * 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. + */ +package com.iciql; + +import java.io.InputStream; +import java.util.Properties; + +import com.iciql.Iciql.Mode; + +/** + * Loads DAO statements from Properties resource files the classpath. + * + * @author James Moger + * + */ +public class DaoClasspathStatementProvider implements DaoStatementProvider { + + private final Properties externalStatements; + + public DaoClasspathStatementProvider() { + externalStatements = load(); + } + + /** + * Returns the list of statement resources to try locating. + * + * @return + */ + protected String[] getStatementResources() { + return new String[] { "/iciql.properties", "/iciql.xml", "/conf/iciql.properties", "/conf/iciql.xml" }; + } + + /** + * Loads the first statement resource found on the classpath. + * + * @return the loaded statements + */ + private Properties load() { + + Properties props = new Properties(); + for (String resource : getStatementResources()) { + + InputStream is = null; + + try { + is = DaoProxy.class.getResourceAsStream(resource); + + if (is != null) { + + if (resource.toLowerCase().endsWith(".xml")) { + // load an .XML statements file + props.loadFromXML(is); + } else { + // load a .Properties statements file + props.load(is); + } + + break; + } + + } catch (Exception e) { + throw new IciqlException(e, "Failed to parse {0}", resource); + } finally { + try { + is.close(); + } catch (Exception e) { + } + } + + } + return props; + } + + @Override + public String getStatement(String idOrStatement, Mode mode) { + final String modePrefix = "%" + mode.name().toLowerCase() + "."; + String value = externalStatements.getProperty(idOrStatement, idOrStatement); + value = externalStatements.getProperty(modePrefix + idOrStatement, value); + return value; + } + +} diff --git a/src/main/java/com/iciql/DaoProxy.java b/src/main/java/com/iciql/DaoProxy.java index db5f911..cafd6f7 100644 --- a/src/main/java/com/iciql/DaoProxy.java +++ b/src/main/java/com/iciql/DaoProxy.java @@ -72,7 +72,7 @@ final class DaoProxy<X extends Dao> implements InvocationHandler, Dao { * @return a proxy object */ @SuppressWarnings("unchecked") - X buildProxy() { + X build() { if (!daoInterface.isInterface()) { throw new IciqlException("Dao {0} must be an interface!", daoInterface.getName()); @@ -115,12 +115,14 @@ final class DaoProxy<X extends Dao> implements InvocationHandler, Dao { } else if (method.isAnnotationPresent(SqlQuery.class)) { String sql = method.getAnnotation(SqlQuery.class).value(); - return executeQuery(method, args, sql); + String statement = db.getDaoStatementProvider().getStatement(sql, db.getMode()); + return executeQuery(method, args, statement); } else if (method.isAnnotationPresent(SqlStatement.class)) { String sql = method.getAnnotation(SqlStatement.class).value(); - return executeStatement(method, args, sql); + String statement = db.getDaoStatementProvider().getStatement(sql, db.getMode()); + return executeStatement(method, args, statement); } else { @@ -220,6 +222,11 @@ final class DaoProxy<X extends Dao> implements InvocationHandler, Dao { objects.add(value); + if (!isArray) { + // we are not returning an array so we break + // the loop and return the first result + break; + } } } catch (SQLException e) { diff --git a/src/main/java/com/iciql/DaoStatementProvider.java b/src/main/java/com/iciql/DaoStatementProvider.java new file mode 100644 index 0000000..7e7522a --- /dev/null +++ b/src/main/java/com/iciql/DaoStatementProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 James Moger. + * + * 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. + */ +package com.iciql; + +import com.iciql.Iciql.Mode; + +/** + * Defines the interface for retrieving externalized DAO statements. + * + * @author James Moger + * + */ +public interface DaoStatementProvider { + + /** + * Returns the statement associated with the id. + * + * @param idOrStatement + * @param mode + * @return the statement + */ + String getStatement(String idOrStatement, Mode mode); +} diff --git a/src/main/java/com/iciql/Db.java b/src/main/java/com/iciql/Db.java index 794417e..7413c8f 100644 --- a/src/main/java/com/iciql/Db.java +++ b/src/main/java/com/iciql/Db.java @@ -74,6 +74,7 @@ public class Db implements AutoCloseable { private boolean skipCreate; private boolean autoSavePoint = true; + private DaoStatementProvider daoStatementProvider; static { TOKENS = Collections.synchronizedMap(new WeakIdentityHashMap<Object, Token>()); @@ -102,6 +103,7 @@ public class Db implements AutoCloseable { } dialect = getDialect(databaseName, conn.getClass().getName()); dialect.configureDialect(this); + daoStatementProvider = new NoExternalDaoStatements(); } /** @@ -237,7 +239,30 @@ public class Db implements AutoCloseable { */ @SuppressWarnings("resource") public <X extends Dao> X open(Class<X> daoClass) { - return new DaoProxy<X>(this, daoClass).buildProxy(); + return new DaoProxy<X>(this, daoClass).build(); + } + + /** + * Returns the DAO statement provider. + * + * @return the DAO statement provider + */ + public DaoStatementProvider getDaoStatementProvider() { + return daoStatementProvider; + } + + /** + * Sets the DAO statement provider. + * + * @param statementProvider + */ + public void setDaoStatementProvider(DaoStatementProvider statementProvider) { + if (statementProvider == null) { + throw new IciqlException("You must provide a valid {0} instance!", + DaoStatementProvider.class.getSimpleName()); + } + + this.daoStatementProvider = statementProvider; } /** @@ -841,14 +866,12 @@ public class Db implements AutoCloseable { } /** - * - * @author James Moger - * + * Default DAO statement provider. */ class NoExternalDaoStatements implements DaoStatementProvider { @Override - public String getStatement(String idOrStatement) { + public String getStatement(String idOrStatement, Mode mode) { return idOrStatement; } diff --git a/src/site/dao.mkd b/src/site/dao.mkd index 660c7af..491d085 100644 --- a/src/site/dao.mkd +++ b/src/site/dao.mkd @@ -135,3 +135,46 @@ public interface MyDao extends Dao { }
---JAVA---
+### Runtime Mode & External Statements
+
+Sometimes you may need to specify a slightly different SQL statement for a database engine you might be using in development but not in production. For example, you might develop with H2 and deploy with PostgreSQL.
+
+Being able to switch the DAO statements executed based on the runtime mode would be helpful for some scenarios. Iciql supports this use-case with a `DaoStatementProvider` and provides three mode options: `DEV`, `TEST`, and `PROD`.
+
+#### External Statement DAO Example
+---JAVA---
+public interface MyDao extends Dao {
+ @SqlQuery("some.query")
+ Product [] getProductsWithRuntimeModeDependentQuery();
+}
+
+Db db = Db.open("jdbc:h2:mem:iciql");
+// set a classpath statement resource provider
+db.setDaoStatementProvider(new DaoClasspathStatementProvider());
+
+// open the dao and retrieve the products
+MyDao dao = db.open(MyDao.class);
+Product [] products = dao.getProductsWithRuntimeModeDependentQuery();
+---JAVA---
+
+#### External Statement Resource Example
+
+---FIXED---
+some.query = select * from Products # default statement
+%prod.some.query = select * from Products # will be used in PROD mode
+%test.some.query = select * from Products where category = 'Beverages' # will be used in TEST mode
+%dev.some.query = select * from Products where category = 'Condiments' # will be used in DEV mode
+---FIXED---
+
+#### DaoClasspathStatementProvider
+
+Iciql ships with one useful implementation of a DaoStatementProvider: `DaoClasspathStatementProvider`.
+
+This provider will load a single external statement resource from the classpath, if found. It tries to locate one of the following classpath resources and loads the first one identified using the `java.util.Properties` class.
+
+1. `/iciql.properties`
+2. `/iciql.xml`
+3. `/conf/iciql.properties`
+4. `/conf/iciql.xml`
+
+Every `@SqlQuery` and `@SqlStatement` method will ask the `DaoStatementProvider` for the statement to execute based on the annotation value and the runtime mode. For the `DaoClasspathStatementProvider`, if the annotation value is not a key in the resource file it is assumed to be a statement and is returned to the DAO object for execution. This allows you to externalize a handful of statements - or all of them if you do not want to hard-code anything.
\ No newline at end of file diff --git a/src/site/index.mkd b/src/site/index.mkd index b9fc549..1c742f6 100644 --- a/src/site/index.mkd +++ b/src/site/index.mkd @@ -5,7 +5,7 @@ iciql **is**... - a model-based, database access wrapper for JDBC
- for modest database schemas and basic statement generation
- for those who want to write code, instead of SQL, using IDE completion and compile-time type-safety
-- small (225KB) with debug symbols and no runtime dependencies
+- small (<250KB) with debug symbols and no runtime dependencies
- pronounced *icicle* (although it could be French: *ici ql* - here query language)
- a friendly fork of the H2 [JaQu][jaqu] subproject
@@ -66,7 +66,7 @@ public interface MyDao extends Dao { @TypeAdapter(InvoiceAdapterImpl.class)
public @interface InvoiceAdapter { }
-// Crate a DAO instance with your Db and work more clearly
+// Create a DAO instance with your Db and work more clearly
try (Db db = Db.open("jdbc:h2:mem:iciql")) {
MyDao dao = db.open(MyDao.class);
@@ -91,6 +91,10 @@ This is very useful for mapping your field domain models to SQL without having t You might use this to take advantage of the underlying database's type system. For example, PostgreSQL ships with the compelling JSON/JSONB/XML data types. Iciql provides String and Object adapters to facilitate use of those data types.
+### runtime mode support
+
+Mode support allows you to tweak the behavior of your `@TypeAdapter` and `DAO` implementations to adapt to runtime conditions such as developing on a different database than you deploy on.
+
### Supported Databases (Unit-Tested)
- [H2](http://h2database.com) ${h2.version}
- [HSQLDB](http://hsqldb.org) ${hsqldb.version}
diff --git a/src/test/java/com/iciql/test/IciqlSuite.java b/src/test/java/com/iciql/test/IciqlSuite.java index c088ff9..c80da93 100644 --- a/src/test/java/com/iciql/test/IciqlSuite.java +++ b/src/test/java/com/iciql/test/IciqlSuite.java @@ -48,6 +48,7 @@ import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters;
import com.iciql.Constants;
import com.iciql.Db;
+import com.iciql.Iciql.Mode;
import com.iciql.test.DataTypeAdapterTest.SerializedObjectTypeAdapterTest;
import com.iciql.test.models.BooleanModel;
import com.iciql.test.models.CategoryAnnotationOnly;
@@ -144,13 +145,18 @@ public class IciqlSuite { return Math.abs(expected - actual) <= 0.000001d;
}
+ public static Db openNewDb() {
+ return openNewDb(Mode.PROD);
+ }
+
/**
* Open a new Db object. All connections are cached and re-used to eliminate
* embedded database startup costs.
*
+ * @param mode
* @return a fresh Db object
*/
- public static Db openNewDb() {
+ public static Db openNewDb(Mode mode) {
String testUrl = System.getProperty("iciql.url", DEFAULT_TEST_DB.url);
String testUser = System.getProperty("iciql.user", DEFAULT_TEST_DB.username);
String testPassword = System.getProperty("iciql.password", DEFAULT_TEST_DB.password);
@@ -168,7 +174,7 @@ public class IciqlSuite { dataSources.put(testUrl, dataSource);
connectionFactories.put(testUrl, factory);
}
- db = Db.open(dataSource);
+ db = Db.open(dataSource, mode);
// drop views
db.dropView(ProductView.class);
diff --git a/src/test/java/com/iciql/test/ProductDaoTest.java b/src/test/java/com/iciql/test/ProductDaoTest.java index 78987fd..e5549c1 100644 --- a/src/test/java/com/iciql/test/ProductDaoTest.java +++ b/src/test/java/com/iciql/test/ProductDaoTest.java @@ -26,7 +26,9 @@ import org.junit.Before; import org.junit.Test; import com.iciql.Dao; +import com.iciql.DaoClasspathStatementProvider; import com.iciql.Db; +import com.iciql.Iciql.Mode; import com.iciql.IciqlException; import com.iciql.test.DataTypeAdapterTest.SerializedObjectTypeAdapterTest; import com.iciql.test.DataTypeAdapterTest.SupportedTypesAdapter; @@ -48,6 +50,7 @@ public class ProductDaoTest extends Assert { db = IciqlSuite.openNewDb(); db.insertAll(Product.getList()); db.insertAll(Order.getList()); + db.setDaoStatementProvider(new DaoClasspathStatementProvider()); } @After @@ -273,6 +276,47 @@ public class ProductDaoTest extends Assert { assertTrue(obj1.equivalentTo(obj2)); } + @Test + public void testDefaultProdResourceQueryReturnModels() { + + ProductDao dao = db.open(ProductDao.class); + + Product[] products = dao.getProductsFromResourceQuery(); + assertEquals(10, products.length); + } + + @Test + public void testDevResourceQueryReturnModels() { + + Db db = IciqlSuite.openNewDb(Mode.DEV); + db.insertAll(Product.getList()); + db.insertAll(Order.getList()); + db.setDaoStatementProvider(new DaoClasspathStatementProvider()); + + ProductDao dao = db.open(ProductDao.class); + + Product[] products = dao.getProductsFromResourceQuery(); + assertEquals(5, products.length); + + db.close(); + } + + @Test + public void testTestResourceQueryReturnModels() { + + Db db = IciqlSuite.openNewDb(Mode.TEST); + db.insertAll(Product.getList()); + db.insertAll(Order.getList()); + db.setDaoStatementProvider(new DaoClasspathStatementProvider()); + + ProductDao dao = db.open(ProductDao.class); + + Product[] products = dao.getProductsFromResourceQuery(); + assertEquals(2, products.length); + + db.close(); + } + /** * Define the Product DAO interface. */ @@ -320,7 +364,8 @@ public class ProductDaoTest extends Assert { @SqlQuery("select productId from Product where category = :category") long[] getProductIdsForCategory(@Bind("category") String cat); - @SqlQuery("select orderDate from Orders order by orderDate desc limit 1") + // will break ResultSet iteration after retrieving first value + @SqlQuery("select orderDate from Orders order by orderDate desc") Date getMostRecentOrder(); @SqlStatement("update Product set productName = 'test' where productId = 1") @@ -335,12 +380,16 @@ public class ProductDaoTest extends Assert { @SqlStatement("update Product set category = :newCategory where category = :oldCategory") int renameProductCategoryReturnsCount(@Bind("oldCategory") String oldCategory, @Bind("newCategory") String newCategory); - @SqlQuery("select obj from dataTypeAdapters limit 1") + // will break ResultSet iteration after retrieving first value + @SqlQuery("select obj from dataTypeAdapters") @SupportedTypesAdapter SupportedTypes getCustomDataType(); @SqlStatement("update dataTypeAdapters set obj=:2 where id=:1") boolean setSupportedTypes(long id, @SupportedTypesAdapter SupportedTypes obj); + @SqlQuery("get.products") + Product[] getProductsFromResourceQuery(); + } } diff --git a/src/test/java/iciql.properties b/src/test/java/iciql.properties new file mode 100644 index 0000000..cecb056 --- /dev/null +++ b/src/test/java/iciql.properties @@ -0,0 +1,7 @@ +# +# Example resource file for DaoClasspathStatementProvider +# + +get.products = select * from Product +%test.get.products = select * from Product where category = 'Beverages' +%dev.get.products = select * from Product where category = 'Condiments' |