All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.heliorm.sql.SqlOrm Maven / Gradle / Ivy

The newest version!
package com.heliorm.sql;

import com.heliorm.Database;
import com.heliorm.Field;
import com.heliorm.Orm;
import com.heliorm.OrmException;
import com.heliorm.OrmTransaction;
import com.heliorm.OrmTransactionException;
import com.heliorm.Table;
import com.heliorm.UncaughtOrmException;
import com.heliorm.def.Join;
import com.heliorm.def.Select;
import com.heliorm.def.Where;
import com.heliorm.impl.ExecutablePart;
import com.heliorm.impl.JoinPart;
import com.heliorm.impl.SelectPart;
import com.heliorm.impl.Selector;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.RecordComponent;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static com.heliorm.sql.AbstractionHelper.explodeAbstractions;
import static com.heliorm.sql.AbstractionHelper.makeComparatorForTail;
import static java.lang.String.format;

/**
 * A SQL implementation of the ORM.
 *
 * @author gideon
 */
public final class SqlOrm implements Orm {

    private final SqlDriver driver;
    private final Supplier connectionSupplier;
    private final PojoOperations pops;

    private final Selector selector;
    private final Map, Table> tables = new ConcurrentHashMap<>();
    private final Map, String> inserts = new ConcurrentHashMap<>();
    private final Map, String> updates = new ConcurrentHashMap<>();
    private final Map, String> deletes = new ConcurrentHashMap<>();
    private final Map, Boolean> exists = new ConcurrentHashMap<>();
    private final QueryHelper queryHelper;
    private final PojoHelper pojoHelper;
    private final PreparedStatementHelper preparedStatementHelper;
    private final ResultSetHelper resultSetHelper;
    private SqlTransaction currentTransaction;


    /**
     * Create an ORM mapper using the supplied driver instance. This is meant to
     * be used with third party drivers.
     *
     * @param driver The driver used to access data.
     */
    SqlOrm(SqlDriver driver, Supplier connectionSupplier, PojoOperations pops) {
        this.driver = driver;
        this.connectionSupplier = connectionSupplier;
        this.pops = pops;
        this.queryHelper = new QueryHelper(driver, this::getUniqueFieldName, this::fullTableName);
        this.pojoHelper = new PojoHelper(pops);
        this.preparedStatementHelper = new PreparedStatementHelper(pojoHelper, driver::setEnum);
        this.resultSetHelper = new ResultSetHelper(pops, this::getUniqueFieldName);
        selector = new Selector() {
            @Override
            public  List list(Select tail) throws OrmException {
                return SqlOrm.this.list((SelectPart) tail);
            }

            @Override
            public  Stream stream(Select tail) throws OrmException {
                return SqlOrm.this.stream((SelectPart) tail);
            }

            @Override
            public  Optional optional(Select tail) throws OrmException {
                return SqlOrm.this.optional((SelectPart) tail);
            }

            @Override
            public  O one(Select tail) throws OrmException {
                return SqlOrm.this.one((SelectPart) tail);
            }
        };
    }

    @Override
    public  O create(O pojo) throws OrmException {
        if (pojo == null) {
            throw new OrmException("Attempt to create a null POJO");
        }
        var table = tableFor(pojo);
        var query = inserts.get(table);
        if (query == null) {
            query = queryHelper.buildInsertQuery(table);
            inserts.put(table, query);
        }
        var con = getConnection();
        try (var stmt = con.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) {
            int par = 1;
            for (var field : table.getFields()) {
                if (!field.isPrimaryKey() || !field.isAutoNumber()) {
                    preparedStatementHelper.setValueInStatement(stmt, pojo, field, par);
                    par++;
                }
            }
            stmt.executeUpdate();
            var opt = table.getPrimaryKey();
            if (opt.isPresent()) {
                var keyField = opt.get();
                if (keyField.isAutoNumber()) {
                    try (ResultSet rs = stmt.getGeneratedKeys()) {
                        if (rs.next()) {
                            var keyValue = driver.getKeyValueFromResultSet(rs, keyField);
                            return table.isRecord()
                                    ? newRecordFrom(pojo, keyField, keyValue)
                                    : newPojoFrom(pojo, keyField, keyValue);
                        }
                    }
                }
            }
            return table.isRecord() ? newRecordFrom(pojo) : newPojoFrom(pojo);
        } catch (SQLException ex) {
            throw new OrmSqlException(ex.getMessage(), ex);
        } finally {
            closeConnection(con);
        }
    }

    @Override
    public  O update(O pojo) throws OrmException {
        if (pojo == null) {
            throw new OrmException("Attempt to update a null POJO");
        }
        Table table = tableFor(pojo);
        String query = updates.get(table);
        if (query == null) {
            query = queryHelper.buildUpdateQuery(table);
            updates.put(table, query);
        }
        Connection con = getConnection();
        try (PreparedStatement stmt = con.prepareStatement(query)) {
            int par = 1;
            for (Field field : table.getFields()) {
                if (!field.isPrimaryKey()) {
                    preparedStatementHelper.setValueInStatement(stmt, pojo, field, par);
                    par++;
                }
            }
            Optional> primaryKey = table.getPrimaryKey();
            if (primaryKey.isPresent()) {
                Field keyField = primaryKey.get();
                Object val = pojoHelper.getValueFromPojo(pojo, keyField);
                if (val == null) {
                    throw new OrmException(format("No value for key %s for %s in update", keyField.getJavaName(), table.getObjectClass().getSimpleName()));
                }
                preparedStatementHelper.setValueInStatement(stmt, pojo, keyField, par);
                int modified = stmt.executeUpdate();
                if (modified == 0) {
                    throw new OrmException(format("The update did not modify any data for %s with key field/value %s/%s. (Row does not exist)",
                            table.getObjectClass().getSimpleName(),
                            keyField.getJavaName(),
                            val));
                }
                return pojo;
            } else {
                throw new OrmException(format("No primary key for %s in update", table.getObjectClass().getSimpleName()));
            }
        } catch (SQLException ex) {
            throw new OrmSqlException(ex.getMessage(), ex);
        } finally {
            closeConnection(con);
        }

    }

    @Override
    public  void delete(O pojo) throws OrmException {
        if (pojo == null) {
            throw new OrmException("Attempt to delete a null POJO");
        }
        Table table = tableFor(pojo);
        String query = deletes.get(table);
        if (query == null) {
            query = queryHelper.buildDeleteQuery(table);
            deletes.put(table, query);
        }
        Connection con = getConnection();
        try (PreparedStatement stmt = con.prepareStatement(query)) {
            Optional> primaryKey = table.getPrimaryKey();
            if (primaryKey.isPresent()) {
                preparedStatementHelper.setValueInStatement(stmt, pojo, primaryKey.get(), 1);
            } else {
                throw new OrmException(format("No primary key for %s in delete", table.getObjectClass().getSimpleName()));
            }
            stmt.executeUpdate();
        } catch (SQLException ex) {
            throw new OrmSqlException(ex.getMessage(), ex);
        } finally {
            closeConnection(con);
        }
    }

    @Override
    public  Select select(Table table) {
        return new SelectPart<>(selector(), table);
    }

    @Override
    public  Select select(Table table, Where where) {
        return new SelectPart<>(selector(), table, where, Collections.emptyList());
    }

    @SafeVarargs
    @Override
    public final  Select select(Table table, Join... joins) {
        List> list = Arrays.stream(joins)
                .map(join -> (JoinPart) join).collect(Collectors.toList());
        return new SelectPart<>(selector(), table, null, list);
    }

    @SafeVarargs
    @Override
    public final  Select select(Table table, Where where, Join... joins) {
        List> list = Arrays.stream(joins)
                .map(join -> (JoinPart) join).collect(Collectors.toList());
        return new SelectPart<>(selector(), table, where, list);
    }

    @Override
    public OrmTransaction openTransaction() throws OrmException {
        if (!driver.supportsTransactions()) {
            throw new OrmTransactionException("The ORM driver does not support transactions");
        }
        if (currentTransaction != null) {
            if (currentTransaction.isOpen()) {
                throw new OrmTransactionException("A transaction is already open");
            }
        }
        currentTransaction = new SqlTransaction(driver, getConnection());
        return currentTransaction;
    }

    @Override
    public void close() {
    }

    @Override
    public  Table tableFor(O pojo) throws OrmException {
        //noinspection unchecked
        return tableFor((Class) pojo.getClass());
    }

    @Override
    public  Table tableFor(Class type) throws OrmException {
        if (type == null) {
            throw new OrmException("Attempt to do table lookup for a null class");
        }
        if (tables.isEmpty()) {
            tables.putAll(ServiceLoader.load(Database.class)
                    .stream()
                    .map(ServiceLoader.Provider::get)
                    .map(Database::getTables)
                    .flatMap(List::stream)
                    .collect(Collectors.toMap(Table::getObjectClass, table -> table)));
        }
        @SuppressWarnings("unchecked")
        var table = (Table) tables.get(type);
        if (table == null) {
            throw new OrmException("Cannot find table for pojo of type " + type.getCanonicalName());
        }
        return table;
    }

    @Override
    public Selector selector() {
        return selector;
    }

    private  List list(SelectPart tail) throws OrmException {
        try (Stream stream = stream(tail)) {
            return stream.collect(Collectors.toList());
        }
    }

    private  Stream stream(ExecutablePart tail) throws OrmException {
        List> queries = explodeAbstractions(tail);
        if (queries.isEmpty()) {
            throw new OrmException("Could not build query from parts. BUG!");
        }
        if (queries.size() == 1) {
            var query = queries.getFirst().getSelect();
            Stream> res = streamSingle(query.getTable(), queryHelper.buildSelectQuery(tail));
            return res.map(PojoCompare::getPojo);
        } else {
            if (driver.useUnionAll()) {
                Map> tableMap = queries.stream()
                        .map(query -> query.getSelect().getTable())
                        .collect(Collectors.toMap(table -> table.getObjectClass().getName(), table -> table));
                Stream> sorted = streamUnion(queryHelper.buildSelectUnionQuery(queries.stream().map(ExecutablePart::getSelect).collect(Collectors.toList())), tableMap)
                        .map(pojo -> new PojoCompare<>(pops, tail.getSelect().getTable(), pojo))
                        .sorted(makeComparatorForTail(tail.getOrder()));
                return sorted.map(PojoCompare::getPojo);
            } else {
                Stream> res = queries.stream()
                        .flatMap(select -> {
                            try {
                                return streamSingle(select.getSelect().getTable(), queryHelper.buildSelectQuery(select));
                            } catch (OrmException ex) {
                                throw new UncaughtOrmException(ex.getMessage(), ex);
                            }
                        });
                res = res.distinct();
                if (queries.size() > 1) {
                    if (!tail.getOrder().isEmpty()) {
                        res = res.sorted(makeComparatorForTail(tail.getOrder()));
                    } else {
                        res = res.sorted();
                    }
                }
                return res.map(PojoCompare::getPojo);
            }
        }
    }


    /**
     * Create a stream for the given query on the given table and return a
     * stream referencing the data.
     *
     * @param    The type of the POJOs returned
     * @param table The table on which to query
     * @param query The SQL query
     * @return The stream of results.
     * @throws OrmException Thrown if there are SQL or ORM errors
     */
    private  Stream> streamSingle(Table table, String query) throws OrmException {
        Connection con = getConnection();
        try {
            Statement stmt = con.createStatement();
            ResultSet rs = stmt.executeQuery(query);
            Stream> stream = StreamSupport.stream(
                    Spliterators.spliteratorUnknownSize(new Iterator<>() {
                                                            @Override
                                                            public boolean hasNext() {
                                                                try {
                                                                    return rs.next();
                                                                } catch (SQLException ex) {
                                                                    throw new UncaughtOrmException(ex.getMessage(), ex);
                                                                }
                                                            }

                                                            @Override
                                                            public PojoCompare next() {
                                                                try {
                                                                    return new PojoCompare<>(pops, table, resultSetHelper.makeObjectFromResultSet(rs, table));
                                                                } catch (OrmException ex) {
                                                                    throw new UncaughtOrmException(ex.getMessage(), ex);
                                                                }
                                                            }
                                                        },
                            Spliterator.ORDERED), false);
            return stream.onClose(() -> cleanup(con, stmt, rs));
        } catch (SQLException | UncaughtOrmException ex) {
            cleanup(con, null, null);
            throw new OrmSqlException(ex.getMessage(), ex);
        }
    }

    private  Stream streamUnion(String query, Map> tables) throws OrmException {
        Connection con = getConnection();
        try {
            Statement stmt = con.createStatement();
            ResultSet rs = stmt.executeQuery(query);
            Stream stream = StreamSupport.stream(
                    Spliterators.spliteratorUnknownSize(new Iterator<>() {
                        @Override
                        public boolean hasNext() {
                            try {
                                return rs.next();
                            } catch (SQLException ex) {
                                throw new UncaughtOrmException(ex.getMessage(), ex);
                            }
                        }

                        @Override
                        public O next() {
                            try {
                                return resultSetHelper.makeObjectFromResultSet(rs, tables.get(rs.getString(QueryHelper.POJO_NAME_FIELD)));
                            } catch (OrmException | SQLException ex) {
                                throw new UncaughtOrmException(ex.getMessage(), ex);
                            }
                        }
                    }, Spliterator.ORDERED), false);
            return stream.onClose(() -> cleanup(con, stmt, rs));
        } catch (SQLException | UncaughtOrmException ex) {
            cleanup(con, null, null);
            throw new OrmSqlException(ex.getMessage(), ex);
        }
    }

    private  Optional optional(SelectPart tail) throws OrmException {
        try (Stream stream = stream(tail)) {
            O one;
            Iterator iterator = stream.iterator();
            if (iterator.hasNext()) {
                one = iterator.next();
            } else {
                return Optional.empty();
            }
            if (iterator.hasNext()) {
                throw new OrmException(format("Required one or none %s but found more than one", tail.getTable().getObjectClass().getSimpleName()));
            }
            return Optional.of(one);
        }
    }

    private  O one(SelectPart tail) throws OrmException {
        try (Stream stream = stream(tail)) {
            Iterator iterator = stream.iterator();
            O one;
            if (iterator.hasNext()) {
                one = iterator.next();
            } else {
                throw new OrmException(format("Required exactly one %s but found none", tail.getTable().getObjectClass().getSimpleName()));
            }
            if (iterator.hasNext()) {
                throw new OrmException(format("Required exactly one %s but found more than one", tail.getTable().getObjectClass().getSimpleName()));
            }
            return one;
        }
    }

    /**
     * Obtain the SQL connection to use
     *
     * @return The connection
     */
    private Connection getConnection() {
        if (currentTransaction != null) {
            if (currentTransaction.isOpen()) {
                return currentTransaction.getConnection();
            }
            currentTransaction = null;
        }
        return connectionSupplier.get();
    }

    /**
     * Close a SQL Connection in a way that properly deals with transactions.
     *
     * @param con The SQL connection
     * @throws OrmException Thrown if there are SQL errors
     */
    private void closeConnection(Connection con) throws OrmException {
        if ((currentTransaction == null) || (currentTransaction.getConnection() != con)) {
            try {
                con.close();
            } catch (SQLException ex) {
                throw new OrmException(ex.getMessage(), ex);
            }
        }
    }

    /**
     * Cleanup SQL Connection, Statement and ResultSet insuring that
     * errors will not result in aborted cleanup.
     *
     * @param con  The SQL connection
     * @param stmt The SQL statement
     * @param rs   The SQL result set
     */
    private void cleanup(Connection con, Statement stmt, ResultSet rs) {
        Exception error = null;
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception ex) {
                error = ex;
            }
        }
        if (stmt != null) {
            try {
                stmt.close();
            } catch (Exception ex) {
                error = error != null ? error : ex;
            }
        }
        try {
            closeConnection(con);
        } catch (Exception ex) {
            error = error != null ? error : ex;
        }
        if (error != null) {
            throw new UncaughtOrmException(error.getMessage(), error);
        }
    }

    /**
     * Check if a table exists, and create if it does not
     *
     * @param table The table to check
     * @throws OrmException Thrown if there are SQL or ORM errors
     */
    private void checkTable(Table table) throws OrmException {
        if (driver.createTables()) {
            if (!exists.containsKey(table)) {
                if (!tableExists(table)) {
                    Connection con = getConnection();
                    try (Statement stmt = con.createStatement()) {
                        stmt.executeUpdate(driver.getTableGenerator().generateSchema(table));
                    } catch (SQLException ex) {
                        throw new OrmSqlException(format("Error creating table (%s)", ex.getMessage()), ex);
                    } finally {
                        closeConnection(con);
                    }
                }
                exists.put(table, Boolean.TRUE);
            }
        }
    }

    /**
     * Get the full table name for a table.
     *
     * @param table The table
     * @return The table name
     * @throws OrmException Thrown if there are SQL or ORM errors
     */
    private String fullTableName(Table table) throws OrmException {
        checkTable(table);
        return driver.fullTableName(table);
    }

    /**
     * Determine if the SQL table exists for a table structure
     *
     * @param table The Table
     * @return True if it exists in the database
     * @throws OrmException Thrown if there are SQL or ORM errors
     */
    private boolean tableExists(Table table) throws OrmException {
        Connection con = getConnection();
        try {
            DatabaseMetaData dbm = con.getMetaData();
            try (ResultSet tables = dbm.getTables(driver.databaseName(table), null, driver.makeTableName(table), null)) {
                return tables.next();
            }
        } catch (SQLException ex) {
            throw new OrmSqlException(format("Error checking table existence (%s)", ex.getMessage()), ex);
        } finally {
            closeConnection(con);
        }
    }

    /**
     * Get the unique field name for a field
     *
     * @param field The field
     * @return The ID
     */
    private String getUniqueFieldName(Field field) {
        return format("%s_%s", field.getTable().getSqlTable(), field.getSqlName());
    }

    private  O newRecordFrom(O obj, Field field, Object value) throws OrmException {
        var table = tableFor(obj);
        var cons = findCanononicalConstructor(table);
        try {
            return cons.newInstance(table.getFields().stream()
                    .map(f -> f.getJavaName().equals(field.getJavaName()) ? value : getComponentValue(obj, f))
                    .toArray());
        } catch (InstantiationException | InvocationTargetException | IllegalAccessException | UncaughtOrmException e) {
            throw new OrmException(e.getMessage(), e);
        }
    }

    private  O newRecordFrom(O obj) throws OrmException {
        var table = tableFor(obj);
        var cons = findCanononicalConstructor(table);
        try {
            return cons.newInstance(table.getFields().stream()
                    .map(f -> getComponentValue(obj, f))
                    .toArray());
        } catch (InstantiationException | InvocationTargetException | IllegalAccessException | UncaughtOrmException e) {
            throw new OrmException(e.getMessage(), e);
        }
    }

    private  O newPojoFrom(O obj) throws OrmException {
        var table = tableFor(obj);
        var newPojo = pops.newPojoInstance(table);
        for (var f : table.getFields()) {
            pops.setValue(newPojo, f, pops.getValue(obj, f));
        }
        return newPojo;
    }

    private  O newPojoFrom(O obj, Field field, Object value) throws OrmException {
        var table = tableFor(obj);
        var newPojo = pops.newPojoInstance(table);
        for (var f : table.getFields()) {
            if (f.getJavaName().equals(field.getJavaName())) {
                pops.setValue(newPojo, f, value);
            } else {
                pops.setValue(newPojo, f, pops.getValue(obj, f));
            }
        }
        return newPojo;
    }

    private static  Constructor findCanononicalConstructor(Table table) throws OrmException {
        try {
            return table.getObjectClass().getDeclaredConstructor(Arrays.stream(table.getObjectClass().getRecordComponents())
                    .map(RecordComponent::getType).toList().toArray(new Class[]{}));
        } catch (NoSuchMethodException e) {
            throw new OrmException(e.getMessage(), e);
        }
    }

    private static  Object getComponentValue(O obj, Field field) {
        var opt = Arrays.stream(obj.getClass().getRecordComponents())
                .filter(com -> com.getName().equals(field.getJavaName()))
                .findFirst();
        if (opt.isEmpty()) {
            throw new UncaughtOrmException(format("Cannot find record component for field '%s' on %s", field.getJavaName(), obj.getClass().getSimpleName()));
        }
        var meth = opt.get().getAccessor();
        try {
            return meth.invoke(obj);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new UncaughtOrmException(e.getMessage(), e);
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy