- a model-based, database access wrapper for JDBC\r
- for modest database schemas and basic statement generation\r
- for those who want to write code, instead of SQL, using IDE completion and compile-time type-safety\r
-- small (225KB with debug symbols) with no runtime dependencies\r
+- small (<250KB with debug symbols) with no runtime dependencies\r
- pronounced *icicle* (although it could be French: *ici ql* - here query language)\r
- a friendly fork of the H2 [JaQu](http://h2database.com/html/jaqu.html) project\r
\r
--- /dev/null
+/*
+ * 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;
+ }
+
+}
* @return a proxy object
*/
@SuppressWarnings("unchecked")
- X buildProxy() {
+ X build() {
if (!daoInterface.isInterface()) {
throw new IciqlException("Dao {0} must be an interface!", daoInterface.getName());
} 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 {
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) {
--- /dev/null
+/*
+ * 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);
+}
private boolean skipCreate;
private boolean autoSavePoint = true;
+ private DaoStatementProvider daoStatementProvider;
static {
TOKENS = Collections.synchronizedMap(new WeakIdentityHashMap<Object, Token>());
}
dialect = getDialect(databaseName, conn.getClass().getName());
dialect.configureDialect(this);
+ daoStatementProvider = new NoExternalDaoStatements();
}
/**
*/
@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;
}
/**
}
/**
- *
- * @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;
}
}\r
---JAVA---\r
\r
+### Runtime Mode & External Statements\r
+\r
+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.\r
+\r
+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`.\r
+\r
+#### External Statement DAO Example\r
+---JAVA---\r
+public interface MyDao extends Dao {\r
+ @SqlQuery("some.query")\r
+ Product [] getProductsWithRuntimeModeDependentQuery();\r
+}\r
+\r
+Db db = Db.open("jdbc:h2:mem:iciql");\r
+// set a classpath statement resource provider\r
+db.setDaoStatementProvider(new DaoClasspathStatementProvider());\r
+\r
+// open the dao and retrieve the products\r
+MyDao dao = db.open(MyDao.class);\r
+Product [] products = dao.getProductsWithRuntimeModeDependentQuery();\r
+---JAVA---\r
+\r
+#### External Statement Resource Example\r
+\r
+---FIXED---\r
+some.query = select * from Products # default statement\r
+%prod.some.query = select * from Products # will be used in PROD mode\r
+%test.some.query = select * from Products where category = 'Beverages' # will be used in TEST mode\r
+%dev.some.query = select * from Products where category = 'Condiments' # will be used in DEV mode\r
+---FIXED---\r
+\r
+#### DaoClasspathStatementProvider\r
+\r
+Iciql ships with one useful implementation of a DaoStatementProvider: `DaoClasspathStatementProvider`.\r
+\r
+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.\r
+\r
+1. `/iciql.properties`\r
+2. `/iciql.xml`\r
+3. `/conf/iciql.properties`\r
+4. `/conf/iciql.xml`\r
+\r
+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
- a model-based, database access wrapper for JDBC\r
- for modest database schemas and basic statement generation\r
- for those who want to write code, instead of SQL, using IDE completion and compile-time type-safety\r
-- small (225KB) with debug symbols and no runtime dependencies\r
+- small (<250KB) with debug symbols and no runtime dependencies\r
- pronounced *icicle* (although it could be French: *ici ql* - here query language)\r
- a friendly fork of the H2 [JaQu][jaqu] subproject\r
\r
@TypeAdapter(InvoiceAdapterImpl.class)\r
public @interface InvoiceAdapter { }\r
\r
-// Crate a DAO instance with your Db and work more clearly\r
+// Create a DAO instance with your Db and work more clearly\r
try (Db db = Db.open("jdbc:h2:mem:iciql")) {\r
\r
MyDao dao = db.open(MyDao.class);\r
\r
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.\r
\r
+### runtime mode support\r
+\r
+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.\r
+\r
### Supported Databases (Unit-Tested)\r
- [H2](http://h2database.com) ${h2.version}\r
- [HSQLDB](http://hsqldb.org) ${hsqldb.version}\r
import com.beust.jcommander.Parameters;\r
import com.iciql.Constants;\r
import com.iciql.Db;\r
+import com.iciql.Iciql.Mode;\r
import com.iciql.test.DataTypeAdapterTest.SerializedObjectTypeAdapterTest;\r
import com.iciql.test.models.BooleanModel;\r
import com.iciql.test.models.CategoryAnnotationOnly;\r
return Math.abs(expected - actual) <= 0.000001d;\r
}\r
\r
+ public static Db openNewDb() {\r
+ return openNewDb(Mode.PROD);\r
+ }\r
+\r
/**\r
* Open a new Db object. All connections are cached and re-used to eliminate\r
* embedded database startup costs.\r
*\r
+ * @param mode\r
* @return a fresh Db object\r
*/\r
- public static Db openNewDb() {\r
+ public static Db openNewDb(Mode mode) {\r
String testUrl = System.getProperty("iciql.url", DEFAULT_TEST_DB.url);\r
String testUser = System.getProperty("iciql.user", DEFAULT_TEST_DB.username);\r
String testPassword = System.getProperty("iciql.password", DEFAULT_TEST_DB.password);\r
dataSources.put(testUrl, dataSource);\r
connectionFactories.put(testUrl, factory);\r
}\r
- db = Db.open(dataSource);\r
+ db = Db.open(dataSource, mode);\r
\r
// drop views\r
db.dropView(ProductView.class);\r
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;
db = IciqlSuite.openNewDb();
db.insertAll(Product.getList());
db.insertAll(Order.getList());
+ db.setDaoStatementProvider(new DaoClasspathStatementProvider());
}
@After
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.
*/
@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")
@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();
+
}
}
--- /dev/null
+#
+# 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'