From: James Moger Date: Mon, 10 Nov 2014 16:59:09 +0000 (-0500) Subject: Implement DAO externalized statement loading based on runtime Mode X-Git-Tag: v1.5.0~3 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=a6df2de41953e10db1527e54acd734c0f0a1fa28;p=iciql.git Implement DAO externalized statement loading based on runtime Mode --- 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 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 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 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()); @@ -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 open(Class daoClass) { - return new DaoProxy(this, daoClass).buildProxy(); + return new DaoProxy(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'