You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Db.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. /*
  2. * Copyright 2004-2011 H2 Group.
  3. * Copyright 2011 James Moger.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package com.iciql;
  18. import java.sql.Connection;
  19. import java.sql.PreparedStatement;
  20. import java.sql.ResultSet;
  21. import java.sql.SQLException;
  22. import java.sql.Statement;
  23. import java.util.ArrayList;
  24. import java.util.Collections;
  25. import java.util.HashMap;
  26. import java.util.HashSet;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Properties;
  30. import java.util.Set;
  31. import javax.sql.DataSource;
  32. import com.iciql.DbUpgrader.DefaultDbUpgrader;
  33. import com.iciql.Iciql.IQDatabase;
  34. import com.iciql.Iciql.IQTable;
  35. import com.iciql.SQLDialect.DefaultSQLDialect;
  36. import com.iciql.SQLDialect.H2Dialect;
  37. import com.iciql.util.JdbcUtils;
  38. import com.iciql.util.StringUtils;
  39. import com.iciql.util.Utils;
  40. import com.iciql.util.WeakIdentityHashMap;
  41. /**
  42. * This class represents a connection to a database.
  43. */
  44. public class Db {
  45. /**
  46. * This map It holds unique tokens that are generated by functions such as
  47. * Function.sum(..) in "db.from(p).select(Function.sum(p.unitPrice))". It
  48. * doesn't actually hold column tokens, as those are bound to the query
  49. * itself.
  50. */
  51. private static final Map<Object, Token> TOKENS;
  52. private static final Map<String, Class<? extends SQLDialect>> DIALECTS;
  53. private final Connection conn;
  54. private final Map<Class<?>, TableDefinition<?>> classMap = Collections
  55. .synchronizedMap(new HashMap<Class<?>, TableDefinition<?>>());
  56. private final SQLDialect dialect;
  57. private DbUpgrader dbUpgrader = new DefaultDbUpgrader();
  58. private final Set<Class<?>> upgradeChecked = Collections.synchronizedSet(new HashSet<Class<?>>());
  59. static {
  60. TOKENS = Collections.synchronizedMap(new WeakIdentityHashMap<Object, Token>());
  61. DIALECTS = Collections.synchronizedMap(new HashMap<String, Class<? extends SQLDialect>>());
  62. DIALECTS.put("org.h2", H2Dialect.class);
  63. }
  64. private Db(Connection conn) {
  65. this.conn = conn;
  66. dialect = getDialect(conn.getClass().getCanonicalName());
  67. dialect.configureDialect(conn);
  68. }
  69. public static void registerDialect(Connection conn, Class<? extends SQLDialect> dialectClass) {
  70. registerDialect(conn.getClass().getCanonicalName(), dialectClass);
  71. }
  72. public static void registerDialect(String connClass, Class<? extends SQLDialect> dialectClass) {
  73. DIALECTS.put(connClass, dialectClass);
  74. }
  75. SQLDialect getDialect(String clazz) {
  76. // try dialect by connection class name
  77. Class<? extends SQLDialect> dialectClass = DIALECTS.get(clazz);
  78. int lastDot = 0;
  79. while (dialectClass == null) {
  80. // try dialect by package name
  81. int nextDot = clazz.indexOf('.', lastDot);
  82. if (nextDot > -1) {
  83. String pkg = clazz.substring(0, nextDot);
  84. lastDot = nextDot + 1;
  85. dialectClass = DIALECTS.get(pkg);
  86. } else {
  87. dialectClass = DefaultSQLDialect.class;
  88. }
  89. }
  90. return instance(dialectClass);
  91. }
  92. static <X> X registerToken(X x, Token token) {
  93. TOKENS.put(x, token);
  94. return x;
  95. }
  96. static Token getToken(Object x) {
  97. return TOKENS.get(x);
  98. }
  99. private static <T> T instance(Class<T> clazz) {
  100. try {
  101. return clazz.newInstance();
  102. } catch (Exception e) {
  103. throw new IciqlException(e);
  104. }
  105. }
  106. public static Db open(String url, String user, String password) {
  107. try {
  108. Connection conn = JdbcUtils.getConnection(null, url, user, password);
  109. return new Db(conn);
  110. } catch (SQLException e) {
  111. throw convert(e);
  112. }
  113. }
  114. /**
  115. * Create a new database instance using a data source. This method is fast,
  116. * so that you can always call open() / close() on usage.
  117. *
  118. * @param ds
  119. * the data source
  120. * @return the database instance.
  121. */
  122. public static Db open(DataSource ds) {
  123. try {
  124. return new Db(ds.getConnection());
  125. } catch (SQLException e) {
  126. throw convert(e);
  127. }
  128. }
  129. public static Db open(Connection conn) {
  130. return new Db(conn);
  131. }
  132. public static Db open(String url, String user, char[] password) {
  133. try {
  134. Properties prop = new Properties();
  135. prop.setProperty("user", user);
  136. prop.put("password", password);
  137. Connection conn = JdbcUtils.getConnection(null, url, prop);
  138. return new Db(conn);
  139. } catch (SQLException e) {
  140. throw convert(e);
  141. }
  142. }
  143. private static Error convert(Exception e) {
  144. return new Error(e);
  145. }
  146. public <T> void insert(T t) {
  147. Class<?> clazz = t.getClass();
  148. define(clazz).createTableIfRequired(this).insert(this, t, false);
  149. }
  150. public <T> long insertAndGetKey(T t) {
  151. Class<?> clazz = t.getClass();
  152. return define(clazz).createTableIfRequired(this).insert(this, t, true);
  153. }
  154. /**
  155. * Merge usually INSERTS if the record does not exist or UPDATES the record
  156. * if it does exist. Not all databases support MERGE and the syntax varies
  157. * with the database.
  158. *
  159. * If the dialect does not support merge an IciqlException will be thrown.
  160. *
  161. * @param t
  162. */
  163. public <T> void merge(T t) {
  164. if (!getDialect().supportsMerge()) {
  165. throw new IciqlException("Merge is not supported by this SQL dialect");
  166. }
  167. Class<?> clazz = t.getClass();
  168. define(clazz).createTableIfRequired(this).merge(this, t);
  169. }
  170. public <T> void update(T t) {
  171. Class<?> clazz = t.getClass();
  172. define(clazz).createTableIfRequired(this).update(this, t);
  173. }
  174. public <T> void delete(T t) {
  175. Class<?> clazz = t.getClass();
  176. define(clazz).createTableIfRequired(this).delete(this, t);
  177. }
  178. public <T extends Object> Query<T> from(T alias) {
  179. Class<?> clazz = alias.getClass();
  180. define(clazz).createTableIfRequired(this);
  181. return Query.from(this, alias);
  182. }
  183. @SuppressWarnings("unchecked")
  184. public <T> List<T> buildObjects(Class<? extends T> modelClass, ResultSet rs) {
  185. List<T> result = new ArrayList<T>();
  186. TableDefinition<T> def = (TableDefinition<T>) define(modelClass).createTableIfRequired(this);
  187. try {
  188. while (rs.next()) {
  189. T item = Utils.newObject(modelClass);
  190. def.readRow(item, rs);
  191. result.add(item);
  192. }
  193. } catch (SQLException e) {
  194. throw new IciqlException(e);
  195. }
  196. return result;
  197. }
  198. Db upgradeDb() {
  199. if (!upgradeChecked.contains(dbUpgrader.getClass())) {
  200. // flag as checked immediately because calls are nested.
  201. upgradeChecked.add(dbUpgrader.getClass());
  202. IQDatabase model = dbUpgrader.getClass().getAnnotation(IQDatabase.class);
  203. if (model.version() > 0) {
  204. DbVersion v = new DbVersion();
  205. DbVersion dbVersion =
  206. // (SCHEMA="" && TABLE="") == DATABASE
  207. from(v).where(v.schema).is("").and(v.table).is("").selectFirst();
  208. if (dbVersion == null) {
  209. // database has no version registration, but model specifies
  210. // version: insert DbVersion entry and return.
  211. DbVersion newDb = new DbVersion(model.version());
  212. insert(newDb);
  213. } else {
  214. // database has a version registration:
  215. // check to see if upgrade is required.
  216. if ((model.version() > dbVersion.version) && (dbUpgrader != null)) {
  217. // database is an older version than the model
  218. boolean success = dbUpgrader
  219. .upgradeDatabase(this, dbVersion.version, model.version());
  220. if (success) {
  221. dbVersion.version = model.version();
  222. update(dbVersion);
  223. }
  224. }
  225. }
  226. }
  227. }
  228. return this;
  229. }
  230. <T> void upgradeTable(TableDefinition<T> model) {
  231. if (!upgradeChecked.contains(model.getModelClass())) {
  232. // flag is checked immediately because calls are nested
  233. upgradeChecked.add(model.getModelClass());
  234. if (model.tableVersion > 0) {
  235. // table is using iciql version tracking.
  236. DbVersion v = new DbVersion();
  237. String schema = StringUtils.isNullOrEmpty(model.schemaName) ? "" : model.schemaName;
  238. DbVersion dbVersion = from(v).where(v.schema).like(schema).and(v.table).like(model.tableName)
  239. .selectFirst();
  240. if (dbVersion == null) {
  241. // table has no version registration, but model specifies
  242. // version: insert DbVersion entry
  243. DbVersion newTable = new DbVersion(model.tableVersion);
  244. newTable.schema = schema;
  245. newTable.table = model.tableName;
  246. insert(newTable);
  247. } else {
  248. // table has a version registration:
  249. // check if upgrade is required
  250. if ((model.tableVersion > dbVersion.version) && (dbUpgrader != null)) {
  251. // table is an older version than model
  252. boolean success = dbUpgrader.upgradeTable(this, schema, model.tableName,
  253. dbVersion.version, model.tableVersion);
  254. if (success) {
  255. dbVersion.version = model.tableVersion;
  256. update(dbVersion);
  257. }
  258. }
  259. }
  260. }
  261. }
  262. }
  263. <T> TableDefinition<T> define(Class<T> clazz) {
  264. TableDefinition<T> def = getTableDefinition(clazz);
  265. if (def == null) {
  266. upgradeDb();
  267. def = new TableDefinition<T>(clazz);
  268. def.mapFields();
  269. classMap.put(clazz, def);
  270. if (Iciql.class.isAssignableFrom(clazz)) {
  271. T t = instance(clazz);
  272. Iciql table = (Iciql) t;
  273. Define.define(def, table);
  274. } else if (clazz.isAnnotationPresent(IQTable.class)) {
  275. // annotated classes skip the Define().define() static
  276. // initializer
  277. T t = instance(clazz);
  278. def.mapObject(t);
  279. }
  280. }
  281. return def;
  282. }
  283. public synchronized void setDbUpgrader(DbUpgrader upgrader) {
  284. if (!upgrader.getClass().isAnnotationPresent(IQDatabase.class)) {
  285. throw new IciqlException("DbUpgrader must be annotated with " + IQDatabase.class.getSimpleName());
  286. }
  287. this.dbUpgrader = upgrader;
  288. upgradeChecked.clear();
  289. }
  290. SQLDialect getDialect() {
  291. return dialect;
  292. }
  293. public Connection getConnection() {
  294. return conn;
  295. }
  296. public void close() {
  297. try {
  298. conn.close();
  299. } catch (Exception e) {
  300. throw new IciqlException(e);
  301. }
  302. }
  303. public <A> TestCondition<A> test(A x) {
  304. return new TestCondition<A>(x);
  305. }
  306. public <T> void insertAll(List<T> list) {
  307. for (T t : list) {
  308. insert(t);
  309. }
  310. }
  311. public <T> List<Long> insertAllAndGetKeys(List<T> list) {
  312. List<Long> identities = new ArrayList<Long>();
  313. for (T t : list) {
  314. identities.add(insertAndGetKey(t));
  315. }
  316. return identities;
  317. }
  318. public <T> void updateAll(List<T> list) {
  319. for (T t : list) {
  320. update(t);
  321. }
  322. }
  323. public <T> void deleteAll(List<T> list) {
  324. for (T t : list) {
  325. delete(t);
  326. }
  327. }
  328. PreparedStatement prepare(String sql, boolean returnGeneratedKeys) {
  329. try {
  330. if (returnGeneratedKeys) {
  331. return conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
  332. }
  333. return conn.prepareStatement(sql);
  334. } catch (SQLException e) {
  335. throw new IciqlException(e);
  336. }
  337. }
  338. @SuppressWarnings("unchecked")
  339. <T> TableDefinition<T> getTableDefinition(Class<T> clazz) {
  340. return (TableDefinition<T>) classMap.get(clazz);
  341. }
  342. /**
  343. * Run a SQL query directly against the database.
  344. *
  345. * Be sure to close the ResultSet with
  346. *
  347. * <pre>
  348. * JdbcUtils.closeSilently(rs, true);
  349. * </pre>
  350. *
  351. * @param sql
  352. * the SQL statement
  353. * @param args
  354. * optional object arguments for x=? tokens in query
  355. * @return the result set
  356. */
  357. public ResultSet executeQuery(String sql, Object... args) {
  358. try {
  359. if (args.length == 0) {
  360. return conn.createStatement().executeQuery(sql);
  361. } else {
  362. PreparedStatement stat = conn.prepareStatement(sql);
  363. int i = 1;
  364. for (Object arg : args) {
  365. stat.setObject(i++, arg);
  366. }
  367. return stat.executeQuery();
  368. }
  369. } catch (SQLException e) {
  370. throw new IciqlException(e);
  371. }
  372. }
  373. /**
  374. * Run a SQL query directly against the database and map the results to the
  375. * model class.
  376. *
  377. * @param modelClass
  378. * the model class to bind the query ResultSet rows into.
  379. * @param sql
  380. * the SQL statement
  381. * @return the result set
  382. */
  383. public <T> List<T> executeQuery(Class<? extends T> modelClass, String sql, Object... args) {
  384. ResultSet rs = null;
  385. try {
  386. if (args.length == 0) {
  387. rs = conn.createStatement().executeQuery(sql);
  388. } else {
  389. PreparedStatement stat = conn.prepareStatement(sql);
  390. int i = 1;
  391. for (Object arg : args) {
  392. stat.setObject(i++, arg);
  393. }
  394. rs = stat.executeQuery();
  395. }
  396. return buildObjects(modelClass, rs);
  397. } catch (SQLException e) {
  398. throw new IciqlException(e);
  399. } finally {
  400. JdbcUtils.closeSilently(rs, true);
  401. }
  402. }
  403. /**
  404. * Run a SQL statement directly against the database.
  405. *
  406. * @param sql
  407. * the SQL statement
  408. * @return the update count
  409. */
  410. public int executeUpdate(String sql) {
  411. Statement stat = null;
  412. try {
  413. stat = conn.createStatement();
  414. int updateCount = stat.executeUpdate(sql);
  415. return updateCount;
  416. } catch (SQLException e) {
  417. throw new IciqlException(e);
  418. } finally {
  419. JdbcUtils.closeSilently(stat);
  420. }
  421. }
  422. }