io.stallion.dataAccess.db.DB.java Source code

Java tutorial

Introduction

Here is the source code for io.stallion.dataAccess.db.DB.java

Source

/*
 * Stallion Core: A Modern Web Framework
 *
 * Copyright (C) 2015 - 2016 Stallion Software LLC.
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation, either version 2 of
 * the License, or (at your option) any later version. This program is distributed in the hope that
 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
 * License for more details. You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
 *
 *
 *
 */

package io.stallion.dataAccess.db;

import com.mchange.v2.c3p0.ComboPooledDataSource;

import io.stallion.dataAccess.*;
import io.stallion.dataAccess.db.converters.*;
import io.stallion.exceptions.ConfigException;
import io.stallion.exceptions.UsageException;
import io.stallion.plugins.javascript.BaseJavascriptModel;
import io.stallion.services.Log;
import io.stallion.reflection.PropertyUtils;
import io.stallion.settings.Settings;
import io.stallion.settings.childSections.DbConfig;
import io.stallion.utils.Literals;
import io.stallion.utils.StallionClassLoader;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.commons.dbutils.handlers.ColumnListHandler;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.apache.commons.lang3.StringUtils;

import javax.persistence.Column;
import javax.persistence.Table;
import javax.sql.DataSource;
import java.beans.PropertyVetoException;
import java.lang.reflect.Field;
import java.sql.*;
import java.lang.reflect.Method;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*;

import static io.stallion.utils.Literals.*;
import static io.stallion.Context.*;

/**
 * A singleton that stores the database connection pool, registered model schemas,
 * and provides a bunch of helper methods for actually talking to the database.
 */
public class DB {
    //@Converter(converterName = "")
    private List<Schema> tables = new ArrayList<Schema>();
    private Map<String, Schema> classToSchema = new HashMap<String, Schema>();
    private DataSource dataSource;
    private Map<String, AttributeConverter> converters = new HashMap<String, AttributeConverter>();
    private Tickets tickets;

    private DbImplementation dbImplementation;
    private static boolean useDummyPersisterForSqlGenerationMode = false;

    private static DB _instance;

    public static DB instance() {
        return _instance;
    }

    public static boolean available() {
        if (_instance != null) {
            return true;
        }
        return false;
    }

    /**
     * Load the database based on the configuration defined in the stallion.toml settings.
     * @return
     */
    public static DB load() {
        if (emptyInstance(Settings.instance().getDatabase()) || empty(Settings.instance().getDatabase().getUrl())) {
            return null;
        }
        Properties p = new Properties(System.getProperties());
        p.put("com.mchange.v2.log.MLog", "com.mchange.v2.log.FallbackMLog");
        p.put("com.mchange.v2.log.FallbackMLog.DEFAULT_CUTOFF_LEVEL", "OFF"); // or any other
        System.setProperties(p);

        DbConfig config = Settings.instance().getDatabase();
        DB db;
        if (empty(config.getDbAccessorClass())
                || "io.stallion.dataAccess.db.DB".equals(config.getDbAccessorClass())) {
            db = new DB();
        } else {
            Class cls = StallionClassLoader.loadClass(config.getDbAccessorClass());
            if (cls == null) {
                throw new ConfigException(
                        "The dbAccessorClass you chose in the database settings could not be found: "
                                + config.getDbAccessorClass());
            }
            try {
                db = (DB) cls.newInstance();
            } catch (InstantiationException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }

        db.initialize(config);

        _instance = db;
        return _instance;
    }

    /**
     * Close the datasource pool, null out the singleton instance.
     */
    public static void shutdown() {
        if (_instance != null) {
            if (_instance.getDataSource() instanceof ComboPooledDataSource) {
                ((ComboPooledDataSource) _instance.getDataSource()).close();
            }
        }
        _instance = null;
    }

    /**
     * Intialize the database based on the passed in configuration object.
     * @param config
     */
    public void initialize(DbConfig config) {
        try {
            dbImplementation = (DbImplementation) StallionClassLoader.loadClass(config.getImplementationClass())
                    .newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        // Test out the connection. We do this directly, because if we test via the ComboPooledDataSource
        // exceptions will make the driver hang while retrying, and will also bury the underlying cause

        try {
            Driver driver = (Driver) StallionClassLoader.loadClass(config.getDriverClass()).newInstance();
            Properties props = new Properties();
            props.setProperty("user", config.getUsername());
            props.setProperty("password", config.getPassword());
            try (Connection conn = driver.connect(config.getUrl(), props)) {
                Statement st = conn.createStatement();
                ResultSet results = st.executeQuery("SELECT 1 AS oneCol");
                results.next();
                Long i = results.getLong("oneCol");
                assert i == 1L;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        ComboPooledDataSource cpds = new ComboPooledDataSource();
        /*
        try {
        try (Connection conn = cpds.getConnection()) {
            Statement st = conn.createStatement();
            ResultSet results = st.executeQuery("SELECT 1");
            Long i = results.getLong(0);
            assert i == 1L;
        }
        } catch (SQLException e) {
        throw new RuntimeException(e);
        }
            
        */
        try {
            cpds.setDriverClass(config.getDriverClass()); //loads the jdbc driver
        } catch (PropertyVetoException e) {
            throw new RuntimeException(e);
        }

        String url = config.getUrl();
        if (!url.contains("?")) {
            url += "?";
        }
        // Assume the database server is in UTC
        if (!url.contains("&useLegacyDatetimeCode=")) {
            url += "&useLegacyDatetimeCode=false";
        }
        if (!url.contains("&serverTimezone=")) {
            url += "&serverTimezone=UTC";
        }
        //&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC

        cpds.setJdbcUrl(url);
        cpds.setUser(config.getUsername());
        cpds.setPassword(config.getPassword());

        if (url.contains("utf8mb4_unicode_ci")) {
            cpds.setConnectionCustomizerClassName("io.stallion.dataAccess.db.mysql.Utf8InitCustomizer");
        }

        cpds.setAcquireRetryAttempts(10);
        cpds.setAcquireRetryDelay(200);
        //cpds.setCheckoutTimeout(1);
        // the settings below are optional -- c3p0 can work with defaults
        cpds.setMinPoolSize(5);
        cpds.setAcquireIncrement(5);
        cpds.setMaxPoolSize(20);
        cpds.setIdleConnectionTestPeriod(5000);
        cpds.setTestConnectionOnCheckin(true);

        this.dataSource = cpds;

        // Make sure the database server time is UTC and in sync with the local server time
        // or else stop execution to prevent nasty and insiduious errors.
        //Timestamp date = this.queryScalar(dbImplementation.getCurrentTimeStampQuery());
        Timestamp date = this.queryScalar(dbImplementation.getCurrentTimeStampQuery());
        ZonedDateTime now = utcNow();
        ZonedDateTime dbTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.of("UTC"));

        //LocalDateTime now = utcNow().toLocalDateTime();
        ZonedDateTime max = now.plusMinutes(2);
        ZonedDateTime min = now.minusMinutes(2);

        //LocalDateTime dbTime = date.toLocalDateTime();
        if (dbTime.isAfter(max) || dbTime.isBefore(min)) {
            throw new ConfigException(
                    "The database CURRENT_TIMESTAMP() is mismatched with the server time. Db time is " + dbTime
                            + ". Server time is " + now
                            + ". Make sure the database server is in UTC and that all your servers clocks are matched. ");
        }

        // Todo: why not lazy load converters???
        registerConverter(new JsonMapConverter());
        registerConverter(new JsonSetConverter());
        registerConverter(new JsonObjectConverter());
        registerConverter(new JsonListConverter());

        this.tickets = dbImplementation.initTicketsService(this);

    }

    /**
     * Instantiate an instance of a Tickets subclass, based on the database type
     * @return
     */
    public Tickets newTicketsGeneratorInstance() {
        return dbImplementation.initTicketsService(this);
    }

    public Class getDefaultPersisterClass() {
        return DbPersister.class;
    }

    public DB registerConverter(AttributeConverter converter) {
        converters.put(converter.getClass().getName(), converter);
        return this;
    }

    /**
     * Returns a QueryRunner instance associated with the underlying dataSource
     * @return
     */
    public QueryRunner newQuery() {
        return new QueryRunner(dataSource);
    }

    /**
     * Update the database with this object, or insert it if it does not exist.
     *
     * @param obj
     * @return
     */
    public Model save(Model obj) {

        if (obj.getId() == null) {

            insert(obj);
            return obj;
        } else if (obj.getIsNewInsert()) {
            insert(obj);
            obj.setIsNewInsert(false);
            return obj;
        } else {
            int affected = update(obj);
            if (affected == 0) {
                insert(obj);
            }
            return obj;
        }
    }

    /**
     * Query the database with arbitrary SQL and return a scalar object (a string, number, boolean, etc).
     *
     * @param sql
     * @param params
     * @param <T>
     * @return
     */
    public <T> T queryScalar(String sql, Object... params) {

        QueryRunner runner = new QueryRunner(dataSource);
        try {
            return runner.query(sql, new ScalarHandler<T>(), params);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * Query the database with arbitrary SQL and return a scalar object (a string, number, boolean, etc).
     *
     * @param sql
     * @param params
     * @param <T>
     * @return
     */
    public <T> List<T> queryColumn(String sql, Object... params) {

        QueryRunner runner = new QueryRunner(dataSource);
        try {
            return runner.query(sql, new ColumnListHandler<T>(), params);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * Fetch all objects of the given model.
     *
     * @param model
     * @param <T>
     * @return
     */
    public <T extends Model> List<T> fetchAll(Class<T> model) {
        return fetchAll(model, null, null);
    }

    /**
     * Fetch the object with the given id.
     *
     * @param model
     * @param id
     * @param <T>
     * @return
     */
    public <T> T fetchOne(Class<? extends T> model, Object id) {
        return fetchOne(model, "id", id);
    }

    /**
     * Fetch an object where field==value
     *
     * @param model
     * @param field
     * @param value
     * @param <T>
     * @return
     */
    public <T> T fetchOne(Class<? extends T> model, String field, Object value) {
        Schema schema = getSchemaForModelClass(model);
        QueryRunner runner = new QueryRunner(dataSource);
        //ResultSetHandler handler = new BeanHandler(model);
        ResultSetHandler handler = new ModelResultHandler(schema);
        String sql = "SELECT * FROM " + schema.getName() + " WHERE " + field + "=? LIMIT 1";
        T record = null;
        try {
            record = (T) runner.query(sql, handler, value);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return record;
    }

    /**
     * Fetch all objects where column field is of value
     *
     * @param model
     * @param field
     * @param value
     * @param <T>
     * @return
     */
    public <T extends Model> List<T> fetchAll(Class<? extends T> model, String field, Object value) {
        Schema schema = getSchemaForModelClass(model);
        QueryRunner runner = new QueryRunner(dataSource);
        ModelListHandler<T> handler = new ModelListHandler<T>(schema);

        String sql = "SELECT * FROM " + schema.getName();
        if (!empty(field)) {
            sql += " WHERE " + field + "=?";
        }
        List records = null;
        try {
            if (!empty(field)) {
                records = runner.query(sql, handler, value);
            } else {
                records = runner.query(sql, handler);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return records;
    }

    /**
     * Fetch all objects, sorted
     *
     * @param model
     * @param <T>
     * @return
     */
    public <T extends Model> List<T> fetchAllSorted(Class<? extends T> model, String sortField,
            String sortDirection) {
        Schema schema = getSchemaForModelClass(model);
        QueryRunner runner = new QueryRunner(dataSource);
        ModelListHandler<T> handler = new ModelListHandler<T>(schema);
        sortDirection = sortDirection.toUpperCase();
        if (!"ASC".equals(sortDirection) && !"DESC".equals(sortDirection)) {
            throw new UsageException("Invalid sort direction: " + sortDirection);
        }

        if (!"id".equals(sortField) && !"row_updated_at".equals(sortField)
                && !schema.getKeyNames().contains(sortField)) {
            throw new UsageException("Sort field must be a database key. Sort field was: " + sortField
                    + " on model " + model.getCanonicalName());
        }

        String sql = "SELECT * FROM " + schema.getName() + " ORDER BY " + sortField + " " + sortDirection;
        List records = null;
        try {
            records = runner.query(sql, handler);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return records;
    }

    /**
     * Find a list of rows via arbitrary SQL, and return those rows as hashmaps
     *
     * @param sql
     * @param args
     * @return
     */
    public List<Map<String, Object>> findRecords(String sql, Object... args) {
        QueryRunner runner = new QueryRunner(dataSource);
        MapListHandler handler = new MapListHandler();
        try {
            return runner.query(sql, handler, args);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Find one record via arbitrary SQL and return it as a hashmap
     *
     * @param sql
     * @param args
     * @return
     */
    public Map<String, Object> findRecord(String sql, Object... args) {
        QueryRunner runner = new QueryRunner(dataSource);
        MapHandler handler = new MapHandler();
        try {
            return runner.query(sql, handler, args);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Find one object of the given model via arbitrary SQL
     *
     * @param model
     * @param sql
     * @param args
     * @param <T>
     * @return
     */
    public <T extends Model> T queryForOne(Class<T> model, String sql, Object... args) {
        List<T> models = query(model, sql, args);
        if (models.size() < 1) {
            return null;
        } else {
            return models.get(0);
        }
    }

    /**
     * Find a list of objects of the given model via arbitrary SQL
     *
     * @param model
     * @param sql
     * @param args
     * @param <T>
     * @return
     */
    public <T extends Model> List<T> query(Class<T> model, String sql, Object... args) {
        QueryRunner runner = new QueryRunner(dataSource);
        Schema schema = null;
        if (Model.class.isAssignableFrom(model)) {
            schema = getSchemaForModelClass(model);
        }
        if (schema != null) {
            ModelListHandler<T> handler = new ModelListHandler<T>(schema);
            try {
                return runner.query(sql, handler, args);
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        } else {
            BeanListHandler<T> handler = new BeanListHandler(model);
            try {
                return runner.query(sql, handler, args);
            } catch (SQLException e) {
                Log.exception(e.getNextException(), "Root exception in query");
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * Find a list of objects of the given model via arbitrary SQL.
     * Accepts any java bean, does not require a Stallion Model
     *
     * @param model
     * @param sql
     * @param args
     * @param <T>
     * @return
     */
    public <T> List<T> queryBean(Class<T> model, String sql, Object... args) {
        QueryRunner runner = new QueryRunner(dataSource);
        //BeanListHandler<T> handler = new BeanListHandler(model);
        BeanListHandler<T> handler = new BeanListHandler(model);
        try {
            return runner.query(sql, handler, args);
        } catch (SQLException e) {
            Log.exception(e.getNextException(), "Root exception in queryBean");
            throw new RuntimeException(e);
        }
    }

    /**
     * Find a list of objects of the given model via arbitrary SQL.
     * Accepts any java bean, does not require a Stallion Model
     *
     * @param model
     * @param sql
     * @param args
     * @param <T>
     * @return
     */
    public <T> T fetchBean(Class<T> model, String sql, Object... args) {
        QueryRunner runner = new QueryRunner(dataSource);
        //BeanListHandler<T> handler = new BeanListHandler(model);
        BeanListHandler<T> handler = new BeanListHandler(model);
        try {
            List<T> results = runner.query(sql, handler, args);
            if (results.size() == 0) {
                return null;
            } else {
                return results.get(0);
            }
        } catch (SQLException e) {
            Log.exception(e.getNextException(), "Root exception in queryBean");
            throw new RuntimeException(e);
        }
    }

    /**
     * Find a list of objects via arbitrary SQL, checking the cache first, and storing to the
     * cache if retrieved fromt he database.
     *
     * @param model
     * @param sql
     * @param args
     * @param <T>
     * @return
     */
    public <T extends Model> List<T> cachedQuery(Class<T> model, String sql, Object... args) {
        String cacheKey = buildCacheKey(model, sql, args);
        Object result = SmartQueryCache.getSmart(model.getCanonicalName(), cacheKey);
        if (result != null) {
            return (List<T>) result;
        }
        QueryRunner runner = new QueryRunner(dataSource);
        List<T> items = list();
        try {
            Schema schema = getSchemaForModelClass(model);
            if (!emptyInstance(schema)) {
                ModelListHandler<T> handler = new ModelListHandler<T>(schema);
                items = runner.query(sql, handler, args);
            } else {
                BeanListHandler<T> handler = new BeanListHandler(model);
                items = runner.query(sql, handler, args);
            }
            SmartQueryCache.set(model.getCanonicalName(), cacheKey, items);
            return items;
        } catch (SQLException e) {
            SmartQueryCache.set(model.getCanonicalName(), cacheKey, items);
            throw new RuntimeException(e);
        }
    }

    /**
     * Find a single value based on the given model, from arbitrary SQL, and
     * use the cache.
     *
     * @param model
     * @param sql
     * @param params
     * @param <T>
     * @return
     */
    public <T> T cachedScalar(Class model, String sql, Object... params) {
        String cacheKey = buildCacheKey(model, sql, params);
        T result = (T) SmartQueryCache.getSmart(model.getCanonicalName(), cacheKey);
        if (result != null) {
            return result;
        }
        QueryRunner runner = new QueryRunner(dataSource);
        try {
            result = runner.query(sql, new ScalarHandler<T>(), params);
            SmartQueryCache.set(model.getCanonicalName(), cacheKey, result);
            return result;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * Select from the given model, using just an arbitrary WHERE ... clause, and use the cache.
     *
     * @param model
     * @param where
     * @param args
     * @param <T>
     * @return
     */
    public <T extends Model> List<T> cachedWhere(Class<T> model, String where, Object... args) {
        String cacheKey = buildCacheKey(model, where, args);
        Object result = SmartQueryCache.getSmart(model.getCanonicalName(), cacheKey);
        if (result != null) {
            return (List<T>) result;
        }
        Schema schema = getSchemaForModelClass(model);

        QueryRunner runner = new QueryRunner(dataSource);
        //ResultSetHandler handler = new BeanHandler(model);
        String sql = "SELECT * FROM " + schema.getName() + " WHERE " + where;
        ModelListHandler<T> handler = new ModelListHandler<T>(schema);
        List<T> items = list();
        try {
            items = runner.query(sql, handler, args);

            SmartQueryCache.set(model.getCanonicalName(), cacheKey, items);
            return items;
        } catch (SQLException e) {
            SmartQueryCache.set(model.getCanonicalName(), cacheKey, items);
            throw new RuntimeException(e);
        }
    }

    /**
     * Turns a SQL query into a unique md5 hash to be used as a cache key.
     *
     * @param model
     * @param sql
     * @param args
     * @return
     */
    protected String buildCacheKey(Class model, String sql, Object... args) {
        StringBuilder builder = new StringBuilder();
        builder.append("SQLQuery" + Literals.GSEP);
        builder.append(model.getCanonicalName() + Literals.GSEP);
        builder.append(sql + Literals.GSEP);
        for (Object arg : args) {
            builder.append(arg.toString() + Literals.GSEP);
        }
        String fullKey = builder.toString();
        return DigestUtils.md5Hex(fullKey);
    }

    /**
     * Find an object using an arbitrary SQL WHERE ... clause
     *
     * @param model
     * @param where
     * @param args
     * @return
     */
    public Object where(Class model, String where, Object... args) {
        Schema schema = getSchemaForModelClass(model);

        QueryRunner runner = new QueryRunner(dataSource);
        //ResultSetHandler handler = new BeanHandler(model);
        String sql = "SELECT * FROM \"" + schema.getName() + "\" WHERE " + where;
        ModelListHandler handler = new ModelListHandler(schema);
        try {
            return runner.query(sql, handler, args);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Delete the object from the database.
     *
     * @param obj
     */
    public void delete(Model obj) {
        Schema schema = getSchemaForModelClass(obj.getClass());

        String sql = "DELETE FROM " + schema.getName() + " WHERE id=?";
        QueryRunner runner = new QueryRunner(dataSource);
        try {
            runner.update(sql, obj.getId());
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        obj.setDeleted(true);
    }

    /**
     * Update the object
     *
     * @param obj
     * @return rows affected
     */
    public int update(Model obj) {
        Schema schema = getSchemaForModelClass(obj.getClass());

        String sql = "UPDATE `" + schema.getName() + "` SET ";
        List args = new ArrayList<>();
        for (Col col : schema.getColumns()) {
            if (col.getUpdateable()) {
                sql += "`" + col.getName() + "`" + "=?, ";
                Object arg = PropertyUtils.getPropertyOrMappedValue(obj, col.getPropertyName());
                arg = convertColumnArg(obj, col, arg);
                args.add(arg);
            }
        }
        sql = StringUtils.strip(sql.trim(), ",");
        sql += " WHERE id=?";
        args.add(obj.getId());
        QueryRunner run = new QueryRunner(dataSource);
        int affected = 0;
        try {
            affected = run.update(sql, args.toArray());
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return affected;
    }

    /**
     * Update only the passed in key values
     *
     * @param obj
     * @return rows affected
     */
    public int update(Model obj, Map<String, Object> values) {
        if (values.size() == 0) {
            return 0;
        }
        Schema schema = getSchemaForModelClass(obj.getClass());
        String sql = "UPDATE `" + schema.getName() + "` SET ";
        List args = new ArrayList<>();
        for (Col col : schema.getColumns()) {
            if (col.getUpdateable() && values.containsKey(col.getPropertyName())) {
                sql += "`" + col.getName() + "`" + "=?, ";
                Object arg = values.get(col.getPropertyName());
                arg = convertColumnArg(obj, col, arg);
                args.add(arg);
            }
        }
        if (args.size() == 0) {
            return 0;
        }
        sql = StringUtils.strip(sql.trim(), ",");
        sql += " WHERE id=?";
        args.add(obj.getId());
        QueryRunner run = new QueryRunner(dataSource);
        int affected = 0;
        try {
            affected = run.update(sql, args.toArray());
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return affected;
    }

    /**
     * Insert the object into the database.
     *
     * @param obj
     * @return
     */
    public Model insert(Model obj) {
        Schema schema = getSchemaForModelClass(obj.getClass());

        if (obj.getId() == null) {
            obj.setId(dal().getTickets().nextId());
        }
        String sql = "INSERT INTO `" + schema.getName() + "`  (id ";
        List args = new ArrayList<>();
        args.add(obj.getId());
        for (Col col : schema.getColumns()) {
            if (col.getInsertable()) {
                sql += ", `" + col.getName() + "` ";
            }
        }
        sql += ") VALUES(?";
        for (Col col : schema.getColumns()) {
            if (col.getInsertable()) {
                sql += ", ?";
                Object arg = PropertyUtils.getPropertyOrMappedValue(obj, col.getPropertyName());
                if (arg == null && col.getDefaultValue() != null) {
                    arg = col.getDefaultValue();
                    PropertyUtils.setProperty(obj, col.getPropertyName(), arg);
                }
                arg = convertColumnArg(obj, col, arg);
                args.add(arg);
            }
        }
        sql += ") ";
        QueryRunner runner = new QueryRunner(dataSource);
        try {
            runner.update(sql, args.toArray());
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return obj;
    }

    /**
     * Trys to convert java type into what is needed by JDBC to store to the database.
     *
     * @param o
     * @param col
     * @param arg
     * @return
     */
    public Object convertColumnArg(Model o, Col col, Object arg) {

        if (arg == null && col.getDefaultValue() != null) {
            arg = col.getDefaultValue();
        }
        if (col.getAttributeConverter() != null) {
            arg = col.getAttributeConverter().convertToDatabaseColumn(arg);
        } else if (arg != null && !StringUtils.isBlank(col.getConverterClassName())) {
            AttributeConverter converter = this.getConverter(col.getConverterClassName());
            arg = converter.convertToDatabaseColumn(arg);
        } else if (arg instanceof LocalDateTime) {
            arg = new Timestamp(((LocalDateTime) arg).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
        } else if (arg instanceof ZonedDateTime) {
            arg = new Timestamp(((ZonedDateTime) arg).toInstant().toEpochMilli());
        } else if (arg instanceof Enum) {
            return arg.toString();
        }
        return arg;
    }

    /**
     * Add a model to the internal registry of schemas.
     *
     * @param cls
     * @return
     */
    public Schema addModel(Class cls) {
        Schema schema = modelToSchema(cls);
        this.addTable(schema);
        classToSchema.put(schema.getClazz().getName(), schema);
        return schema;
    }

    /**
     * Add a dynamicModelDefinition to the internal registry of schemas
     *
     * @param def
     * @return
     */
    public Schema addDynamicModelDefinition(DynamicModelDefinition def) {
        Schema schema = new Schema(def.getTable(), def.getModelClass());
        schema.setColumns(def.getColumns());
        this.addTable(schema);
        classToSchema.put(def.getTable(), schema);
        return schema;
    }

    Schema getSchemaForModelClass(Class cls) {
        if (BaseJavascriptModel.class.isAssignableFrom(cls)) {
            String bucketName = null;
            try {
                bucketName = ((BaseJavascriptModel) cls.newInstance()).getBucketName();
            } catch (InstantiationException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
            return classToSchema.getOrDefault(bucketName, null);
        } else {
            return classToSchema.getOrDefault(cls.getCanonicalName(), null);
        }
    }

    public Schema modelToSchema(Class cls) {
        try {
            Field metaField = cls.getField("meta");
            if (metaField != null) {
                Map<String, Object> o = (Map<String, Object>) metaField.get(null);
                if (o != null && !empty(o.getOrDefault("tableName", "").toString())) {
                    return metaDataModelToSchema(cls, o);
                }
            }
        } catch (NoSuchFieldException e) {
            Log.exception(e, "Error finding meta field");
        } catch (IllegalAccessException e) {
            Log.exception(e, "Error accesing meta field");
        }
        return annotatedModelToSchema(cls);
    }

    private Schema annotatedModelToSchema(Class cls) {
        Table table = (Table) cls.getDeclaredAnnotation(Table.class);
        if (table == null) {
            throw new UsageException("Trying to register model with database, but it has no @Table annotation: "
                    + cls.getCanonicalName());
        }
        if (empty(table.name())) {
            throw new UsageException(
                    "Trying to register model with database, but it has no name for the @Table annotation: "
                            + cls.getCanonicalName());
        }
        Schema schema = new Schema(table.name(), cls);
        Object inst = null;
        try {
            inst = cls.newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        ExtraKeyDefinitions keyDefinitions = (ExtraKeyDefinitions) cls
                .getDeclaredAnnotation(ExtraKeyDefinitions.class);
        if (keyDefinitions != null) {
            for (String def : keyDefinitions.value()) {
                schema.getExtraKeyDefinitions().add(def);
            }
        }

        for (Method method : cls.getMethods()) {
            if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) {
                continue;
            }
            // If a property starts with "is", the type must be boolean, or else we continue;
            if (method.getName().startsWith("is") && (!method.getReturnType().isAssignableFrom(boolean.class))
                    && !method.getReturnType().isAssignableFrom(Boolean.class)) {
                continue;
            }

            String propertyName = "";
            if (method.getName().startsWith("is")) {
                propertyName = method.getName().substring(2);
            } else {
                propertyName = method.getName().substring(3);
            }
            if (empty(propertyName)) {
                continue;
            }
            propertyName = propertyName.substring(0, 1).toLowerCase() + propertyName.substring(1);
            Column columnAnno = method.getAnnotation(Column.class);
            if (columnAnno == null) {
                continue;
            }

            String columnName = propertyName.toLowerCase();
            if (!StringUtils.isBlank(columnAnno.name())) {
                columnName = columnAnno.name();
            }
            Col col = new Col();
            col.setPropertyName(propertyName);
            col.setName(columnName);
            col.setUpdateable(columnAnno.updatable());
            col.setjType(method.getReturnType());
            col.setInsertable(columnAnno.insertable());

            col.setDbType(columnAnno.columnDefinition());
            col.setNullable(columnAnno.nullable());
            col.setLength(columnAnno.length());
            // If the column cannot be null, we need a default value
            if (!columnAnno.nullable()) {
                col.setDefaultValue(PropertyUtils.getProperty(inst, propertyName));
            }
            VirtualColumn virtualAnno = method.getAnnotation(VirtualColumn.class);
            if (virtualAnno != null) {
                col.setVirtual(true);
                col.setInsertable(false);
                col.setUpdateable(false);
            }

            Converter converterAnno = method.getDeclaredAnnotation(Converter.class);
            Log.finest("Adding schema Column {0}", columnName);
            if (converterAnno != null) {
                Log.finest("ConverterAnno {0} {1} {2} ", columnName, converterAnno, converterAnno.name());
                if (empty(converterAnno.name())) {
                    try {
                        AttributeConverter converter = (AttributeConverter) converterAnno.cls().newInstance();
                        if (converter instanceof TypedCollectionAttributeConverter) {
                            if (converterAnno.elementClass() == null
                                    || converterAnno.elementClass().equals(Object.class)) {
                                throw new UsageException(
                                        "You used a TypedCollectionAttributeConverter but did not set the value for elementClass which controls the type of object that you want to convert to. ");
                            }
                            ((TypedCollectionAttributeConverter) converter)
                                    .setElementClass(converterAnno.elementClass());
                        }
                        col.setAttributeConverter(converter);
                        //col.setConverterClassName(converterAnno.cls().getCanonicalName());
                    } catch (InstantiationException e) {
                        throw new RuntimeException(e);
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }

                } else {
                    col.setConverterClassName(converterAnno.name());
                }
            }
            if (method.getAnnotation(AlternativeKey.class) != null) {
                col.setAlternativeKey(true);
                schema.getKeyNames().add(col.getName());
            }
            if (method.getAnnotation(UniqueKey.class) != null) {
                col.setUniqueKey(true);
                UniqueKey uk = (UniqueKey) method.getAnnotation(UniqueKey.class);
                col.setCaseInsensitive(uk.caseInsensitive());
                schema.getKeyNames().add(col.getName());
            }
            if (columnAnno.unique()) {
                col.setUniqueKey(true);
                schema.getKeyNames().add(col.getName());
            }

            schema.getColumns().add(col);

        }
        return schema;
    }

    public List<Schema> getSchemas() {
        return new ArrayList<>(classToSchema.values());
    }

    private Schema metaDataModelToSchema(Class cls, Map<String, Object> meta) {
        Schema schema = new Schema(meta.get("tableName").toString(), cls);
        for (Map colData : (List<Map>) meta.getOrDefault("columns", list())) {
            Col col = new Col();
            col.setPropertyName(colData.getOrDefault("propertyName", "").toString());
            col.setName(colData.getOrDefault("name", "").toString());
            col.setUpdateable((boolean) colData.getOrDefault("updateable", true));
            col.setInsertable((boolean) colData.getOrDefault("insertable", true));

            schema.getColumns().add(col);
        }

        return schema;
    }

    /**
     * Execute an arbitrary SQL command, return the affected rows.
     *
     * @param sql
     * @param params
     * @return
     */
    public int execute(String sql, Object... params) {
        try {
            return newQuery().update(sql, params);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    DB addTable(Schema schema) {
        tables.add(schema);
        return this;
    }

    /**
     * Get or instantiate an AttributeConverter instance based on the
     * converter className
     *
     * @param className
     * @return
     */
    public AttributeConverter getConverter(String className) {
        if (converters.containsKey(className)) {
            return converters.get(className);
        }
        Class cls = StallionClassLoader.loadClass(className);
        if (cls == null) {
            throw new UsageException("Could not find converter class: " + className);
        }
        try {
            AttributeConverter converter = (AttributeConverter) cls.newInstance();
            converters.put(className, converter);
            return converter;
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

    }

    public boolean tableExists(String tableName) {
        if (tableName.contains("`") || tableName.contains("'")) {
            return false;
        }
        List<Map<String, Object>> tables = DB.instance().findRecords("SHOW TABLES LIKE '" + tableName + "'");
        return tables.size() > 0;
    }

    public boolean columnExists(String tableName, String column) {
        if (!tableExists(tableName)) {
            return false;
        }
        if (tableName.contains("`")) {
            return false;
        }
        List<Map<String, Object>> columns = DB.instance()
                .findRecords("SHOW COLUMNS FROM `" + tableName + "` WHERE field=?", column);
        return columns.size() > 0;
    }

    public SqlAndParams toInQueryParams(List things) {
        SqlAndParams o = new SqlAndParams();
        StringBuffer buf = new StringBuffer();
        buf.append("(");
        for (Object thing : things) {
            buf.append("?,");
        }
        String sql = StringUtils.strip(buf.toString(), ",");
        sql = sql + ")";
        o.setSql(sql);
        o.setParamsList(things);
        return o;
    }

    public static class SqlAndParams {
        private List<Object> paramsList;
        private Object[] params;
        private String sql;

        public List<Object> getParamsList() {
            return paramsList;
        }

        public SqlAndParams setParamsList(List<Object> paramsList) {
            this.paramsList = paramsList;
            return this;
        }

        public Object[] getParams() {
            return asArray(getParamsList(), Object.class);
        }

        public String getSql() {
            return sql;
        }

        public SqlAndParams setSql(String sql) {
            this.sql = sql;
            return this;
        }
    }

    /**
     * Get the table schema for a given model class
     *
     * @param cls
     * @return
     */
    public Schema getSchema(Class cls) {
        return classToSchema.get(cls.getName());
    }

    /**
     * Get the table schema for a model name
     *
     * @param name
     * @return
     */
    public Schema getSchema(String name) {
        return classToSchema.get(name);
    }

    /**
     * Get the underlying database connection pool
     *
     * @return
     */
    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * Get the Tickets service for generating new IDS
     * @return
     */
    public Tickets getTickets() {
        return tickets;
    }

    public DbImplementation getDbImplementation() {
        return dbImplementation;
    }

    /**
     * If true, internally, DalRegistry.register() will never use a DbPersister, instead
     * it will use a DummyPersister. This flag is set to true if we are running the command
     * sql-generate, in which case we need to register all models, but the models will fail
     * because the table does not actually exist. So by using a DummyPersister, we avoid
     * hard failures, allowing model registry to execute, then we can get a list of all registered
     * models and generate the correct SQL.
     *
     * @return
     */
    public static boolean isUseDummyPersisterForSqlGenerationMode() {
        return useDummyPersisterForSqlGenerationMode;
    }

    public static void setUseDummyPersisterForSqlGenerationMode(boolean useDummyPersisterForSqlGenerationMode) {
        DB.useDummyPersisterForSqlGenerationMode = useDummyPersisterForSqlGenerationMode;
    }
}