diff --git a/.github/workflows/maven-deploy-release.yml b/.github/workflows/maven-deploy-release.yml index ed702c9..946804f 100644 --- a/.github/workflows/maven-deploy-release.yml +++ b/.github/workflows/maven-deploy-release.yml @@ -39,7 +39,6 @@ jobs: - name: Deploy to JavaWebStack Repository run: mvn deploy -B -DbuildVersion=${{ github.event.release.tag_name }} -s build/settings.xml -Dmaven.test.skip=true env: - DEPLOYMENT_USERNAME: ${{ secrets.DEPLOYMENT_USERNAME }} - DEPLOYMENT_PASSWORD: ${{ secrets.DEPLOYMENT_PASSWORD }} + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml index 122197d..707ac7b 100644 --- a/.github/workflows/maven-deploy.yml +++ b/.github/workflows/maven-deploy.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest services: mysql: - image: mariadb:latest + image: 'mariadb:10' env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: test @@ -40,7 +40,6 @@ jobs: - name: Deploy to JavaWebStack Repository run: mvn deploy -B -s build/settings.xml -Dmaven.test.skip=true env: - DEPLOYMENT_USERNAME: ${{ secrets.DEPLOYMENT_USERNAME }} - DEPLOYMENT_PASSWORD: ${{ secrets.DEPLOYMENT_PASSWORD }} + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} diff --git a/README.md b/README.md index 0b621a7..d1112ce 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ for(User user : Repo.get(User.class).all()){ org.javawebstack orm - 1.0.1 + 1.0.3 ``` @@ -72,7 +72,7 @@ You also need to add the driver you want to use. JavaWebStack ORM comes with a w mysql mysql-connector-java - 8.0.27 + 8.0.30 compile ``` diff --git a/build/settings.xml b/build/settings.xml index 07e7726..88571fa 100644 --- a/build/settings.xml +++ b/build/settings.xml @@ -5,19 +5,9 @@ > - javawebstack-snapshots - ${env.DEPLOYMENT_USERNAME} - ${env.DEPLOYMENT_PASSWORD} - - - javawebstack-releases - ${env.DEPLOYMENT_USERNAME} - ${env.DEPLOYMENT_PASSWORD} - - - ossrh - JavaWebStack - ${env.OSSRH_PASSWORD} + central + ${env.CENTRAL_USERNAME} + ${env.CENTRAL_PASSWORD} gpg diff --git a/pom.xml b/pom.xml index d102ba0..28d6f36 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ 8 8 - 1.0.2-SNAPSHOT + 1.0.3-SNAPSHOT org.javawebstack @@ -52,19 +52,24 @@ https://github.com/JavaWebStack/orm/tree/master + + + central-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + + org.javawebstack - abstract-data - 1.0.4 + commons + 1.0.0-SNAPSHOT - - org.yaml - snakeyaml - 1.33 + org.javawebstack + abstract-data + 1.0.7-SNAPSHOT - org.atteo evo-inflector @@ -78,25 +83,31 @@ org.junit.jupiter junit-jupiter-engine - 5.9.0 + 5.10.0 test org.apache.commons commons-lang3 - 3.12.0 + 3.13.0 test org.projectlombok lombok - 1.18.24 + 1.18.28 test mysql mysql-connector-java - 8.0.30 + 8.0.33 + test + + + org.duckdb + duckdb_jdbc + 1.3.2.0 test @@ -104,17 +115,13 @@ - maven-deploy-plugin - 3.0.0-M1 - - - default-deploy - deploy - - deploy - - - + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + org.apache.maven.plugins @@ -159,7 +166,7 @@ sign - A313520526A8DFE1C2A30399C35A3D43C557B112 + EC9CCFF8901F0AA22191DCEDD619376246C066D0 gpg --no-tty @@ -175,13 +182,9 @@ - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots + central + https://central.sonatype.com/repository/maven-snapshots/ - - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - diff --git a/src/main/java/org/javawebstack/orm/Model.java b/src/main/java/org/javawebstack/orm/Model.java index 7a60a73..8c46c34 100644 --- a/src/main/java/org/javawebstack/orm/Model.java +++ b/src/main/java/org/javawebstack/orm/Model.java @@ -1,5 +1,6 @@ package org.javawebstack.orm; +import org.javawebstack.commons.inject.Injector; import org.javawebstack.orm.query.Query; import java.lang.reflect.Field; @@ -22,6 +23,9 @@ public class Model { { updateOriginal(); + Injector injector = Repo.get(getClass()).getInfo().getConfig().getInjector(); + if(injector != null) + injector.inject(this); } static { diff --git a/src/main/java/org/javawebstack/orm/ORM.java b/src/main/java/org/javawebstack/orm/ORM.java index 20e42c0..cf7f4f4 100644 --- a/src/main/java/org/javawebstack/orm/ORM.java +++ b/src/main/java/org/javawebstack/orm/ORM.java @@ -1,8 +1,10 @@ package org.javawebstack.orm; +import org.javawebstack.orm.connection.SQL; +import org.javawebstack.orm.connection.pool.SQLPool; +import org.javawebstack.orm.connection.pool.SingletonPool; import org.javawebstack.orm.exception.ORMConfigurationException; import org.javawebstack.orm.migration.AutoMigrator; -import org.javawebstack.orm.wrapper.SQL; import org.reflections.Reflections; import java.lang.reflect.Modifier; @@ -10,37 +12,54 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.logging.Logger; public class ORM { - public static final Logger LOGGER = Logger.getLogger("ORM"); - private static final Map, Repo> repositories = new HashMap<>(); public static Repo repo(Class model) { return (Repo) repositories.get(model); } - public static Repo register(Class model, SQL sql, ORMConfig config) throws ORMConfigurationException { - Repo repo = new Repo<>(model, sql, config); + public static Repo register(Class model, SQLPool pool, ORMConfig config) throws ORMConfigurationException { + Repo repo = new Repo<>(model, pool, config); repositories.put(model, repo); return repo; } - public static Repo register(Class model, SQL sql) throws ORMConfigurationException { - return register(model, sql, new ORMConfig()); + public static Repo register(Class model, SQLPool pool) throws ORMConfigurationException { + return register(model, pool, new ORMConfig()); } - public static void register(Package p, SQL sql, ORMConfig config) throws ORMConfigurationException { + public static void register(Package p, SQLPool pool, ORMConfig config) throws ORMConfigurationException { for (Class model : new Reflections(p.getName()).getSubTypesOf(Model.class)) { if (!Modifier.isAbstract(model.getModifiers())) - ORM.register(model, sql, config); + ORM.register(model, pool, config); } } + public static void register(Package p, SQLPool pool) throws ORMConfigurationException { + register(p, pool, new ORMConfig()); + } + + @Deprecated + public static Repo register(Class model, SQL sql, ORMConfig config) throws ORMConfigurationException { + return register(model, new SingletonPool(sql), config); + } + + @Deprecated + public static Repo register(Class model, SQL sql) throws ORMConfigurationException { + return register(model, new SingletonPool(sql)); + } + + @Deprecated + public static void register(Package p, SQL sql, ORMConfig config) throws ORMConfigurationException { + register(p, new SingletonPool(sql), config); + } + + @Deprecated public static void register(Package p, SQL sql) throws ORMConfigurationException { - register(p, sql, new ORMConfig()); + register(p, new SingletonPool(sql)); } public static void unregister(Class model) { diff --git a/src/main/java/org/javawebstack/orm/ORMConfig.java b/src/main/java/org/javawebstack/orm/ORMConfig.java index 38c14a6..ab88807 100644 --- a/src/main/java/org/javawebstack/orm/ORMConfig.java +++ b/src/main/java/org/javawebstack/orm/ORMConfig.java @@ -1,5 +1,6 @@ package org.javawebstack.orm; +import org.javawebstack.commons.inject.Injector; import org.javawebstack.orm.exception.ORMConfigurationException; import org.javawebstack.orm.mapper.DefaultMapper; import org.javawebstack.orm.mapper.TypeMapper; @@ -15,6 +16,7 @@ public class ORMConfig { private boolean idAutoIncrement = true; private final List typeMappers = new ArrayList<>(); private boolean preventUnnecessaryUpdates = true; + private Injector injector; public ORMConfig() { typeMappers.add(new DefaultMapper()); @@ -39,6 +41,8 @@ public ORMConfig setDefaultSize(int defaultSize) throws ORMConfigurationExceptio } public ORMConfig addTypeMapper(TypeMapper typeMapper) { + if(injector != null) + injector.inject(typeMapper); typeMappers.add(typeMapper); return this; } @@ -112,4 +116,16 @@ public ORMConfig setPreventUnnecessaryUpdates(boolean preventUnnecessaryUpdates) this.preventUnnecessaryUpdates = preventUnnecessaryUpdates; return this; } + + public ORMConfig setInjector(Injector injector) { + this.injector = injector; + if(injector != null) + typeMappers.forEach(injector::inject); + return this; + } + + public Injector getInjector() { + return injector; + } + } diff --git a/src/main/java/org/javawebstack/orm/Repo.java b/src/main/java/org/javawebstack/orm/Repo.java index f831184..02413fc 100644 --- a/src/main/java/org/javawebstack/orm/Repo.java +++ b/src/main/java/org/javawebstack/orm/Repo.java @@ -1,13 +1,14 @@ package org.javawebstack.orm; +import org.javawebstack.orm.connection.pool.PooledSQL; +import org.javawebstack.orm.connection.pool.SQLPool; import org.javawebstack.orm.exception.ORMConfigurationException; import org.javawebstack.orm.exception.ORMQueryException; import org.javawebstack.orm.filter.DefaultQueryFilter; import org.javawebstack.orm.filter.QueryFilter; import org.javawebstack.orm.migration.AutoMigrator; import org.javawebstack.orm.query.Query; -import org.javawebstack.orm.wrapper.SQL; -import org.javawebstack.orm.wrapper.builder.SQLQueryString; +import org.javawebstack.orm.renderer.SQLQueryString; import java.lang.reflect.Field; import java.sql.SQLException; @@ -25,14 +26,14 @@ public static Repo get(Class model) { } private final TableInfo info; - private final SQL connection; + private final SQLPool pool; private final List> observers = new ArrayList<>(); private Accessible accessible; private QueryFilter filter; - public Repo(Class clazz, SQL connection, ORMConfig config) throws ORMConfigurationException { + public Repo(Class clazz, SQLPool pool, ORMConfig config) throws ORMConfigurationException { this.info = new TableInfo(clazz, config); - this.connection = connection; + this.pool = pool; filter = new DefaultQueryFilter(info.getFilterable(), info.getSearchable()); } @@ -119,12 +120,13 @@ private void executeCreate(T entry) { if (map.containsKey(idCol) && map.get(idCol) == null) map.remove(idCol); } - SQLQueryString qs = getConnection().builder().buildInsert(info, map); - SQL connection = Session.current() != null ? Session.current().getConnection() : this.connection; - int id = connection.write(qs.getQuery(), qs.getParameters().toArray()); - if (info.isAutoIncrement()) - info.getField(info.getIdField()).set(entry, id); - entry.setEntryExists(true); + try(PooledSQL connection = pool.get()) { + SQLQueryString qs = connection.builder().buildInsert(info, map); + int id = connection.write(qs.getQuery(), qs.getParameters().toArray()); + if (info.isAutoIncrement()) + info.getField(info.getIdField()).set(entry, id); + entry.setEntryExists(true); + } } catch (SQLException | IllegalAccessException throwables) { throw new ORMQueryException(throwables); } @@ -173,7 +175,7 @@ public void finalDelete(T entry) { } public T refresh(T entry) { - where(info.getIdField(), getId(entry)).refresh(entry); + where(info.getIdField(), getId(entry)).withDeleted().refresh(entry); entry.updateOriginal(); return entry; } @@ -217,8 +219,8 @@ public void autoMigrate() { AutoMigrator.migrate(this); } - public SQL getConnection() { - return connection; + public SQLPool getPool() { + return pool; } public TableInfo getInfo() { diff --git a/src/main/java/org/javawebstack/orm/SQLMapper.java b/src/main/java/org/javawebstack/orm/SQLMapper.java index dd3ebc8..efb3ea8 100644 --- a/src/main/java/org/javawebstack/orm/SQLMapper.java +++ b/src/main/java/org/javawebstack/orm/SQLMapper.java @@ -1,5 +1,6 @@ package org.javawebstack.orm; +import org.javawebstack.commons.inject.Injector; import org.javawebstack.orm.exception.ORMQueryException; import org.javawebstack.orm.mapper.DefaultMapper; import org.javawebstack.orm.mapper.TypeMapper; diff --git a/src/main/java/org/javawebstack/orm/Session.java b/src/main/java/org/javawebstack/orm/Session.java deleted file mode 100644 index 2204d22..0000000 --- a/src/main/java/org/javawebstack/orm/Session.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.javawebstack.orm; - -import org.javawebstack.orm.wrapper.SQL; - -import java.util.function.Consumer; - -public class Session { - - private static ThreadLocal sessions = new ThreadLocal<>(); - - public static Session current() { - return sessions.get(); - } - - private SQL connection; - - private Session() { - - } - - public Session via(SQL connection) { - this.connection = connection; - return this; - } - - public SQL getConnection() { - return connection; - } - - public static void session(Consumer consumer) { - Session session = begin(); - consumer.accept(session); - end(); - } - - public static Session begin() { - Session session = new Session(); - sessions.set(session); - return session; - } - - public static void end() { - sessions.remove(); - } - -} diff --git a/src/main/java/org/javawebstack/orm/TableInfo.java b/src/main/java/org/javawebstack/orm/TableInfo.java index e144417..79471ee 100644 --- a/src/main/java/org/javawebstack/orm/TableInfo.java +++ b/src/main/java/org/javawebstack/orm/TableInfo.java @@ -32,6 +32,7 @@ public class TableInfo { private String relationField; private final Map filterable = new HashMap<>(); private final List searchable = new ArrayList<>(); + private final List indices = new ArrayList<>(); private static final Class[] appliesDefaultSize = { String.class, @@ -105,6 +106,13 @@ private void analyzeTable(Class model) throws ORMConfigurationE if (!fields.containsKey(dates.update())) throw new ORMConfigurationException("Missing dates field '" + dates.update() + "'"); } + + Index[] unfilteredIndices = model.getAnnotationsByType(Index.class); + for (Index index : unfilteredIndices) { + if (index.value().length == 0) + continue; + indices.add(index); + } } private void analyzeColumns(Class model) throws ORMConfigurationException { @@ -281,6 +289,15 @@ public Object getDefault(String fieldName) { public boolean isNotNull(String fieldName) { if (idField.equals(fieldName)) return true; + + if (hasDates()) { + if (fieldName.equals(getCreatedField())) + return true; + if (fieldName.equals(getUpdatedField())) + return true; + } + + return false; } @@ -292,4 +309,7 @@ public String getRelationField() { return relationField; } + public List getIndices() { + return indices; + } } diff --git a/src/main/java/org/javawebstack/orm/annotation/Index.java b/src/main/java/org/javawebstack/orm/annotation/Index.java new file mode 100644 index 0000000..0e77653 --- /dev/null +++ b/src/main/java/org/javawebstack/orm/annotation/Index.java @@ -0,0 +1,25 @@ +package org.javawebstack.orm.annotation; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Repeatable(Index.Indices.class) +public @interface Index { + Type type() default Type.AUTO; + boolean unique() default false; + String id() default ""; + String[] value() default {}; + + enum Type { + AUTO, + BTREE, + HASH + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface Indices { + Index[] value(); + } +} diff --git a/src/main/java/org/javawebstack/orm/wrapper/BaseSQL.java b/src/main/java/org/javawebstack/orm/connection/BaseSQL.java similarity index 98% rename from src/main/java/org/javawebstack/orm/wrapper/BaseSQL.java rename to src/main/java/org/javawebstack/orm/connection/BaseSQL.java index ad0e0b5..63a5ff2 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/BaseSQL.java +++ b/src/main/java/org/javawebstack/orm/connection/BaseSQL.java @@ -1,4 +1,4 @@ -package org.javawebstack.orm.wrapper; +package org.javawebstack.orm.connection; import org.javawebstack.orm.exception.ORMQueryException; diff --git a/src/main/java/org/javawebstack/orm/connection/DuckDB.java b/src/main/java/org/javawebstack/orm/connection/DuckDB.java new file mode 100644 index 0000000..03b3696 --- /dev/null +++ b/src/main/java/org/javawebstack/orm/connection/DuckDB.java @@ -0,0 +1,93 @@ +package org.javawebstack.orm.connection; + +import org.javawebstack.orm.exception.ORMQueryException; +import org.javawebstack.orm.renderer.MySQLQueryStringRenderer; +import org.javawebstack.orm.renderer.QueryStringRenderer; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; + +public class DuckDB extends BaseSQL { + + private Connection c = null; + private final String path; + private final boolean readOnly; + private Map customParams = new HashMap<>(); + + public DuckDB() { + this(null); + } + + public DuckDB(String path) { + this(path, false); + } + + public DuckDB(String path, boolean readOnly) { + this.path = path; + this.readOnly = readOnly; + } + + public DuckDB setCustomParam(String key, String value) { + customParams.put(key, value); + return this; + } + + public SQL fork() { + if(!readOnly) { + throw new ORMQueryException("Forking is only supported for read-only connections"); + } + DuckDB sql = new DuckDB(path, readOnly); + sql.customParams = customParams; + return sql; + } + + public Connection getConnection() { + try { + if (c == null || c.isClosed()) { + try { + Class.forName("org.duckdb.DuckDBDriver"); + Properties params = new Properties(); + params.put("duckdb.read_only", this.readOnly ? "true" : "false"); + params.putAll(customParams); + c = DriverManager.getConnection("jdbc:duckdb:" + (this.path != null ? this.path : ""), params); + } catch (SQLException e) { + System.out.println("Error: at getConnection()[DuckDB.java] SQLException " + e.getMessage()); + } catch (ClassNotFoundException e) { + System.out.println("Error: at getConnection()[DuckDB.java] ClassNotFoundException"); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + try { + if (c == null || c.isClosed()) + throw new ORMQueryException("Connection failed!"); + } catch (SQLException e) { + e.printStackTrace(); + } + return c; + } + + public void close() { + if(c != null) { + try { + if(!c.isClosed()) + c.close(); + } catch (SQLException ignored) {} + c = null; + } + } + + public QueryStringRenderer builder() { + return MySQLQueryStringRenderer.INSTANCE; + } + +} + diff --git a/src/main/java/org/javawebstack/orm/wrapper/MySQL.java b/src/main/java/org/javawebstack/orm/connection/MySQL.java similarity index 57% rename from src/main/java/org/javawebstack/orm/wrapper/MySQL.java rename to src/main/java/org/javawebstack/orm/connection/MySQL.java index 3d71818..a7b0266 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/MySQL.java +++ b/src/main/java/org/javawebstack/orm/connection/MySQL.java @@ -1,12 +1,18 @@ -package org.javawebstack.orm.wrapper; +package org.javawebstack.orm.connection; import org.javawebstack.orm.exception.ORMQueryException; -import org.javawebstack.orm.wrapper.builder.MySQLQueryStringBuilder; -import org.javawebstack.orm.wrapper.builder.QueryStringBuilder; +import org.javawebstack.orm.renderer.MySQLQueryStringRenderer; +import org.javawebstack.orm.renderer.QueryStringRenderer; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; public class MySQL extends BaseSQL { @@ -18,6 +24,7 @@ public class MySQL extends BaseSQL { private final String password; private final long timeout; private long lastQuery = 0; + private Map customParams = new HashMap<>(); public MySQL(String host, int port, String database, String username, String password) { this(host, port, database, username, password, 60); @@ -32,8 +39,15 @@ public MySQL(String host, int port, String database, String username, String pas this.timeout = timeout * 1000L; } + public MySQL setCustomParam(String key, String value) { + customParams.put(key, value); + return this; + } + public SQL fork() { - return new MySQL(host, port, database, username, password, (int) (timeout / 1000L)); + MySQL sql = new MySQL(host, port, database, username, password, (int) (timeout / 1000L)); + sql.customParams = customParams; + return sql; } public Connection getConnection() { @@ -52,7 +66,16 @@ public Connection getConnection() { if (c == null || c.isClosed()) { try { Class.forName("com.mysql.cj.jdbc.Driver"); - c = DriverManager.getConnection("jdbc:mysql://" + this.host + ":" + this.port + "/" + this.database + "?user=" + this.username + "&password=" + this.password + "&autoReconnect=" + true + "&failOverReadOnly=false&maxReconnects=" + 5 + "&UseUnicode=yes&characterEncoding=UTF-8"); + Properties params = new Properties(); + params.put("user", this.username); + params.put("password", this.password); + params.put("autoReconnect", "true"); + params.put("failOverReadOnly", "false"); + params.put("maxReconnects", "5"); + params.put("UseUnicode", "yes"); + params.put("characterEncoding", "UTF-8"); + params.putAll(customParams); + c = DriverManager.getConnection("jdbc:mysql://" + this.host + ":" + this.port + "/" + this.database, params); } catch (SQLException e) { System.out.println("Error: at getConnection()[MySQL.java] SQLException " + e.getMessage()); } catch (ClassNotFoundException e) { @@ -71,10 +94,19 @@ public Connection getConnection() { return c; } - public QueryStringBuilder builder() { - return MySQLQueryStringBuilder.INSTANCE; + public void close() { + if(c != null) { + try { + if(!c.isClosed()) + c.close(); + } catch (SQLException ignored) {} + c = null; + } } + public QueryStringRenderer builder() { + return MySQLQueryStringRenderer.INSTANCE; + } } diff --git a/src/main/java/org/javawebstack/orm/wrapper/QueryLogger.java b/src/main/java/org/javawebstack/orm/connection/QueryLogger.java similarity index 67% rename from src/main/java/org/javawebstack/orm/wrapper/QueryLogger.java rename to src/main/java/org/javawebstack/orm/connection/QueryLogger.java index 21852ae..0e8e315 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/QueryLogger.java +++ b/src/main/java/org/javawebstack/orm/connection/QueryLogger.java @@ -1,4 +1,4 @@ -package org.javawebstack.orm.wrapper; +package org.javawebstack.orm.connection; public interface QueryLogger { diff --git a/src/main/java/org/javawebstack/orm/wrapper/SQL.java b/src/main/java/org/javawebstack/orm/connection/SQL.java similarity index 73% rename from src/main/java/org/javawebstack/orm/wrapper/SQL.java rename to src/main/java/org/javawebstack/orm/connection/SQL.java index 35a0a3b..ed47c6c 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/SQL.java +++ b/src/main/java/org/javawebstack/orm/connection/SQL.java @@ -1,6 +1,6 @@ -package org.javawebstack.orm.wrapper; +package org.javawebstack.orm.connection; -import org.javawebstack.orm.wrapper.builder.QueryStringBuilder; +import org.javawebstack.orm.renderer.QueryStringRenderer; import java.sql.Connection; import java.sql.ResultSet; @@ -15,13 +15,15 @@ public interface SQL { int write(String queryString, Object... parameters) throws SQLException; void close(ResultSet resultSet); + void close(); - QueryStringBuilder builder(); + QueryStringRenderer builder(); void addQueryLogger(QueryLogger logger); void removeQueryLogger(QueryLogger logger); + @Deprecated SQL fork(); } diff --git a/src/main/java/org/javawebstack/orm/wrapper/SQLite.java b/src/main/java/org/javawebstack/orm/connection/SQLite.java similarity index 65% rename from src/main/java/org/javawebstack/orm/wrapper/SQLite.java rename to src/main/java/org/javawebstack/orm/connection/SQLite.java index c07c4d4..ee9a4c2 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/SQLite.java +++ b/src/main/java/org/javawebstack/orm/connection/SQLite.java @@ -1,7 +1,7 @@ -package org.javawebstack.orm.wrapper; +package org.javawebstack.orm.connection; -import org.javawebstack.orm.wrapper.builder.MySQLQueryStringBuilder; -import org.javawebstack.orm.wrapper.builder.QueryStringBuilder; +import org.javawebstack.orm.renderer.MySQLQueryStringRenderer; +import org.javawebstack.orm.renderer.QueryStringRenderer; import java.sql.Connection; import java.sql.DriverManager; @@ -42,8 +42,18 @@ public Connection getConnection() { return c; } - public QueryStringBuilder builder() { - return MySQLQueryStringBuilder.INSTANCE; // TODO Build a custom one for SQLite + public void close() { + if(c != null) { + try { + if(!c.isClosed()) + c.close(); + } catch (SQLException ignored) {} + c = null; + } + } + + public QueryStringRenderer builder() { + return MySQLQueryStringRenderer.INSTANCE; // TODO Build a custom one for SQLite } diff --git a/src/main/java/org/javawebstack/orm/connection/pool/MinMaxScaler.java b/src/main/java/org/javawebstack/orm/connection/pool/MinMaxScaler.java new file mode 100644 index 0000000..3fd3dfd --- /dev/null +++ b/src/main/java/org/javawebstack/orm/connection/pool/MinMaxScaler.java @@ -0,0 +1,18 @@ +package org.javawebstack.orm.connection.pool; + +public class MinMaxScaler implements PoolScaling { + + private final int min; + private final int max; + + public MinMaxScaler(int min, int max) { + this.min = min; + this.max = max; + } + + public int scale(int total, int used) { + int needed = used + 1; + return Math.max(min, Math.min(max, needed)); + } + +} diff --git a/src/main/java/org/javawebstack/orm/connection/pool/PoolScaling.java b/src/main/java/org/javawebstack/orm/connection/pool/PoolScaling.java new file mode 100644 index 0000000..8a582c6 --- /dev/null +++ b/src/main/java/org/javawebstack/orm/connection/pool/PoolScaling.java @@ -0,0 +1,7 @@ +package org.javawebstack.orm.connection.pool; + +public interface PoolScaling { + + int scale(int total, int used); + +} diff --git a/src/main/java/org/javawebstack/orm/connection/pool/PooledSQL.java b/src/main/java/org/javawebstack/orm/connection/pool/PooledSQL.java new file mode 100644 index 0000000..8e4dd44 --- /dev/null +++ b/src/main/java/org/javawebstack/orm/connection/pool/PooledSQL.java @@ -0,0 +1,75 @@ +package org.javawebstack.orm.connection.pool; + +import org.javawebstack.orm.connection.QueryLogger; +import org.javawebstack.orm.connection.SQL; +import org.javawebstack.orm.exception.ORMQueryException; +import org.javawebstack.orm.renderer.QueryStringRenderer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class PooledSQL implements SQL, AutoCloseable { + + private final SQLPool pool; + private final SQL connection; + private boolean closed; + + public PooledSQL(SQLPool pool, SQL connection) { + if(connection == null) + throw new IllegalArgumentException("connection can not be null"); + this.pool = pool; + this.connection = connection; + } + + private void assureOpen() { + if(closed) + throw new ORMQueryException("Pooled connection has already been returned"); + } + + public Connection getConnection() { + assureOpen(); + return null; + } + + public ResultSet read(String queryString, Object... parameters) throws SQLException { + assureOpen(); + return connection.read(queryString, parameters); + } + + public int write(String queryString, Object... parameters) throws SQLException { + assureOpen(); + return connection.write(queryString, parameters); + } + + public void close(ResultSet resultSet) { + assureOpen(); + connection.close(resultSet); + } + + public void close() { + if (closed) + return; + + pool.release(connection); + closed = true; + } + + public QueryStringRenderer builder() { + assureOpen(); + return connection.builder(); + } + + public void addQueryLogger(QueryLogger logger) { + throw new RuntimeException("addQueryLogger can not be executed on a pooled connection, call it on the pool instead"); + } + + public void removeQueryLogger(QueryLogger logger) { + throw new RuntimeException("removeQueryLogger can not be executed on a pooled connection, call it on the pool instead"); + } + + public SQL fork() { + throw new ORMQueryException("Forking is not supported by pooled connections"); + } + +} diff --git a/src/main/java/org/javawebstack/orm/connection/pool/SQLPool.java b/src/main/java/org/javawebstack/orm/connection/pool/SQLPool.java new file mode 100644 index 0000000..417e9e2 --- /dev/null +++ b/src/main/java/org/javawebstack/orm/connection/pool/SQLPool.java @@ -0,0 +1,106 @@ +package org.javawebstack.orm.connection.pool; + +import org.javawebstack.orm.connection.QueryLogger; +import org.javawebstack.orm.connection.SQL; + +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Supplier; + +public class SQLPool { + + private final PoolScaling scaling; + private final Supplier supplier; + private final List connections = new ArrayList<>(); + private final BlockingQueue connectionQueue = new LinkedBlockingQueue<>(); + private final Set loggers = new HashSet<>(); + private boolean closing; + private PoolQueryLogger queryLogger = new PoolQueryLogger(); + + public SQLPool(PoolScaling scaling, Supplier supplier) { + this.scaling = scaling; + this.supplier = supplier; + this.scale(); + } + + public SQLPool(int min, int max, Supplier supplier) { + this(new MinMaxScaler(min, max), supplier); + } + + public PooledSQL get() { + if(closing) + throw new IllegalStateException("Pool has already been closed"); + scale(); + try { + SQL sql = connectionQueue.take(); + scale(); + return new PooledSQL(this, sql); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void release(SQL sql) { + if(sql == null) { + throw new IllegalArgumentException("sql must not be null"); + } + if(!closing) { + if(sql instanceof PooledSQL) { + sql.close(); + return; + } + synchronized (connectionQueue) { + if(!connectionQueue.contains(sql)) + connectionQueue.add(sql); + } + scale(); + } + } + + public void addQueryLogger(QueryLogger logger) { + loggers.add(logger); + } + + public void removeQueryLogger(QueryLogger logger) { + loggers.remove(logger); + } + + public void scale() { + if(closing) + throw new IllegalStateException("Pool has already been closed"); + int newScale = scaling.scale(connections.size(), connections.size() - connectionQueue.size()); + if(newScale == connections.size()) + return; + synchronized (connectionQueue) { + while (newScale > connections.size()) { + SQL sql = supplier.get(); + sql.addQueryLogger(queryLogger); + connections.add(sql); + connectionQueue.add(sql); + } + while (newScale < connections.size() && !connectionQueue.isEmpty()) { + SQL sql = connectionQueue.poll(); + sql.close(); + connections.remove(sql); + } + } + } + + public void close() { + closing = true; + for(SQL connection : connections) { + connection.close(); + } + } + + private class PoolQueryLogger implements QueryLogger { + + public void log(String query, Object[] parameters) { + for(QueryLogger logger : loggers) + logger.log(query, parameters); + } + + } + +} diff --git a/src/main/java/org/javawebstack/orm/connection/pool/SingletonPool.java b/src/main/java/org/javawebstack/orm/connection/pool/SingletonPool.java new file mode 100644 index 0000000..f1ff6de --- /dev/null +++ b/src/main/java/org/javawebstack/orm/connection/pool/SingletonPool.java @@ -0,0 +1,11 @@ +package org.javawebstack.orm.connection.pool; + +import org.javawebstack.orm.connection.SQL; + +public class SingletonPool extends SQLPool { + + public SingletonPool(SQL sql) { + super(new MinMaxScaler(1,1), () -> sql); + } + +} diff --git a/src/main/java/org/javawebstack/orm/migration/AutoMigrator.java b/src/main/java/org/javawebstack/orm/migration/AutoMigrator.java index 0d4044c..e97ab25 100644 --- a/src/main/java/org/javawebstack/orm/migration/AutoMigrator.java +++ b/src/main/java/org/javawebstack/orm/migration/AutoMigrator.java @@ -2,8 +2,11 @@ import org.javawebstack.orm.Repo; import org.javawebstack.orm.TableInfo; +import org.javawebstack.orm.annotation.Index; +import org.javawebstack.orm.connection.pool.PooledSQL; +import org.javawebstack.orm.connection.pool.SQLPool; import org.javawebstack.orm.exception.ORMQueryException; -import org.javawebstack.orm.wrapper.SQL; +import org.javawebstack.orm.connection.SQL; import java.sql.ResultSet; import java.sql.SQLException; @@ -12,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; public class AutoMigrator { @@ -22,101 +26,128 @@ public static void migrate(Repo... repos) { public static void migrate(boolean fresh, Repo... repos) { if (fresh) drop(repos); - Map> tables = new HashMap<>(); + Map> tables = new HashMap<>(); for (Repo repo : repos) { - if (!tables.containsKey(repo.getConnection())) { - tables.put(repo.getConnection(), getTables(repo.getConnection())); + if (!tables.containsKey(repo.getPool())) { + tables.put(repo.getPool(), getTables(repo.getPool())); } - migrateTable(repo.getConnection(), repo.getInfo(), tables.get(repo.getConnection()).contains(repo.getInfo().getTableName())); + migrateTable(repo.getPool(), repo.getInfo(), tables.get(repo.getPool()).contains(repo.getInfo().getTableName())); } } public static void drop(Repo... repos) { for (Repo repo : repos) { - try { - repo.getConnection().write("DROP TABLE `" + repo.getInfo().getTableName() + "`;"); + try(PooledSQL connection = repo.getPool().get()) { + connection.write("DROP TABLE `" + repo.getInfo().getTableName() + "`;"); } catch (SQLException ignored) { } } } - private static void migrateTable(SQL sql, TableInfo info, boolean tableExists) { - List addColumns = new ArrayList<>(); - List updateColumns = new ArrayList<>(); - Map columnKeys = tableExists ? getColumnKeys(sql, info.getTableName()) : new HashMap<>(); - List addValues = new ArrayList<>(); - List updateValues = new ArrayList<>(); - for (String fieldName : info.getFields()) { - String columnName = info.getColumnName(fieldName); - StringBuilder sb = new StringBuilder("`") - .append(columnName) - .append("` "); - sb.append(info.getType(fieldName).name()); - String parameterTypes = info.getTypeParameters(fieldName); + private static void migrateTable(SQLPool pool, TableInfo info, boolean tableExists) { + try(PooledSQL sql = pool.get()) { + List addColumns = new ArrayList<>(); + List updateColumns = new ArrayList<>(); + Map columnKeys = tableExists ? getColumnKeys(sql, info.getTableName()) : new HashMap<>(); + List addValues = new ArrayList<>(); + List updateValues = new ArrayList<>(); + for (String fieldName : info.getFields()) { + String columnName = info.getColumnName(fieldName); + StringBuilder sb = new StringBuilder("`") + .append(columnName) + .append("` "); + sb.append(info.getType(fieldName).name()); + String parameterTypes = info.getTypeParameters(fieldName); - if (parameterTypes != null) - sb.append('(') - .append(parameterTypes) - .append(')'); - sb.append(info.isNotNull(fieldName) ? " NOT NULL" : " NULL"); - if (info.isAutoIncrement() && info.getIdField().equals(fieldName)) - sb.append(" AUTO_INCREMENT"); - if (columnKeys.containsKey(columnName)) { - if (info.getDefault(fieldName) != null) { - sb.append(" DEFAULT(?)"); - updateValues.add(info.getDefault(fieldName)); + if (parameterTypes != null) + sb.append('(') + .append(parameterTypes) + .append(')'); + sb.append(info.isNotNull(fieldName) ? " NOT NULL" : " NULL"); + if (info.isAutoIncrement() && info.getIdField().equals(fieldName)) + sb.append(" AUTO_INCREMENT"); + if (columnKeys.containsKey(columnName)) { + if (info.getDefault(fieldName) != null) { + sb.append(" DEFAULT(?)"); + updateValues.add(info.getDefault(fieldName)); + } + updateColumns.add(sb.toString()); + } else { + if (info.getDefault(fieldName) != null) { + sb.append(" DEFAULT(?)"); + addValues.add(info.getDefault(fieldName)); + } + addColumns.add(sb.toString()); } - updateColumns.add(sb.toString()); - } else { - if (info.getDefault(fieldName) != null) { - sb.append(" DEFAULT(?)"); - addValues.add(info.getDefault(fieldName)); - } - addColumns.add(sb.toString()); } - } - if (info.getPrimaryKey() != null) { - String columnName = info.getColumnName(info.getPrimaryKey()); - if (!columnKeys.containsKey(columnName) || !columnKeys.get(columnName).contains("PRI")) - addColumns.add("PRIMARY KEY (`" + columnName + "`)"); - } - for (String uniqueField : info.getUniqueKeys()) { - String columnName = info.getColumnName(uniqueField); - if (!columnKeys.containsKey(columnName) || !columnKeys.get(columnName).contains("UNI")) - addColumns.add("UNIQUE (`" + columnName + "`)"); - } - if (!tableExists) { - try { - sql.write(new StringBuilder("CREATE TABLE `") - .append(info.getTableName()) - .append("` (") - .append(String.join(",", addColumns)) - .append(") DEFAULT CHARSET=utf8mb4;").toString() - , addValues.toArray()); - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); + if (info.getPrimaryKey() != null) { + String columnName = info.getColumnName(info.getPrimaryKey()); + if (!columnKeys.containsKey(columnName) || !columnKeys.get(columnName).contains("PRI")) + addColumns.add("PRIMARY KEY (`" + columnName + "`)"); } - } else { - if (addColumns.size() > 0) { + for (String uniqueField : info.getUniqueKeys()) { + String columnName = info.getColumnName(uniqueField); + if (!columnKeys.containsKey(columnName) || !columnKeys.get(columnName).contains("UNI")) + addColumns.add("UNIQUE (`" + columnName + "`)"); + } + if (!tableExists) { try { - sql.write(new StringBuilder("ALTER TABLE `") + sql.write(new StringBuilder("CREATE TABLE `") .append(info.getTableName()) - .append("` ADD (") + .append("` (") .append(String.join(",", addColumns)) - .append(");").toString() + .append(") DEFAULT CHARSET=utf8mb4;").toString() , addValues.toArray()); } catch (SQLException throwables) { throw new ORMQueryException(throwables); } + } else { + if (addColumns.size() > 0) { + try { + sql.write(new StringBuilder("ALTER TABLE `") + .append(info.getTableName()) + .append("` ADD (") + .append(String.join(",", addColumns)) + .append(");").toString() + , addValues.toArray()); + } catch (SQLException throwables) { + throw new ORMQueryException(throwables); + } + } + if (updateColumns.size() > 0) { + try { + sql.write(new StringBuilder("ALTER TABLE `") + .append(info.getTableName()) + .append("` ") + .append(updateColumns.stream().map(c -> "MODIFY COLUMN " + c).collect(Collectors.joining(","))) + .append(";").toString() + , updateValues.toArray()); + } catch (SQLException throwables) { + throw new ORMQueryException(throwables); + } + } } - if (updateColumns.size() > 0) { + + List existingIndices = getIndices(sql, info.getTableName()); + for (Index index : info.getIndices()) { + String columns = Stream.of(index.value()).map(info::getColumnName).map(s -> "`" + s + "`").collect(Collectors.joining(",")); + String id = index.id().length() > 0 ? index.id() : "idx_" + String.join("_", index.value()); + if (existingIndices.contains(id)) + continue; + + StringBuilder sb = new StringBuilder("CREATE "); + if (index.unique()) + sb.append("UNIQUE "); + sb.append("INDEX `").append(id).append("` "); + if (index.type() != Index.Type.AUTO) + sb.append("USING ").append(index.type().name()).append(" "); + sb.append("ON `") + .append(info.getTableName()) + .append("` (") + .append(columns) + .append(");"); try { - sql.write(new StringBuilder("ALTER TABLE `") - .append(info.getTableName()) - .append("` ") - .append(updateColumns.stream().map(c -> "MODIFY COLUMN " + c).collect(Collectors.joining(","))) - .append(";").toString() - , updateValues.toArray()); + sql.write(sb.toString()); } catch (SQLException throwables) { throw new ORMQueryException(throwables); } @@ -137,8 +168,21 @@ private static Map getColumnKeys(SQL sql, String tableName) { } } - private static List getTables(SQL sql) { + private static List getIndices(SQL sql, String tableName) { try { + List indices = new ArrayList<>(); + ResultSet rs = sql.read("SHOW INDEX FROM `" + tableName + "`;"); + while (rs.next()) { + indices.add(rs.getString(1)); + } + return indices; + } catch (SQLException throwables) { + throw new ORMQueryException(throwables); + } + } + + private static List getTables(SQLPool pool) { + try(PooledSQL sql = pool.get()) { List tables = new ArrayList<>(); ResultSet rs = sql.read("SHOW TABLES;"); while (rs.next()) { diff --git a/src/main/java/org/javawebstack/orm/query/Query.java b/src/main/java/org/javawebstack/orm/query/Query.java index 24ba5c8..4e9cb10 100644 --- a/src/main/java/org/javawebstack/orm/query/Query.java +++ b/src/main/java/org/javawebstack/orm/query/Query.java @@ -3,10 +3,10 @@ import org.javawebstack.orm.Model; import org.javawebstack.orm.Repo; import org.javawebstack.orm.SQLMapper; -import org.javawebstack.orm.Session; +import org.javawebstack.orm.connection.pool.PooledSQL; import org.javawebstack.orm.exception.ORMQueryException; -import org.javawebstack.orm.wrapper.SQL; -import org.javawebstack.orm.wrapper.builder.SQLQueryString; +import org.javawebstack.orm.connection.SQL; +import org.javawebstack.orm.renderer.SQLQueryString; import java.sql.ResultSet; import java.sql.SQLException; @@ -21,7 +21,6 @@ public class Query { private final Repo repo; private final Class model; - private SQL connection; private List select = new ArrayList<>(); private final QueryGroup where = new QueryGroup<>(); private Integer offset; @@ -40,15 +39,6 @@ public Query(Class model) { public Query(Repo repo, Class model) { this.repo = repo; this.model = model; - Session session = Session.current(); - this.connection = repo.getConnection(); - if(session != null && session.getConnection() != null) - this.connection = session.getConnection(); - } - - public Query via(SQL connection) { - this.connection = connection; - return this; } public boolean isWithDeleted() { @@ -259,11 +249,21 @@ public Query whereExists(Class model, Function, return this; } + public Query whereNotExists(Class model, Function, Query> consumer) { + where.whereNotExists(model, consumer); + return this; + } + public Query orWhereExists(Class model, Function, Query> consumer) { where.orWhereExists(model, consumer); return this; } + public Query orWhereNotExists(Class model, Function, Query> consumer) { + where.orWhereNotExists(model, consumer); + return this; + } + public Query whereIn(Object left, Object... values) { where.whereIn(left, values); return this; @@ -361,12 +361,18 @@ public Query withDeleted() { return this; } + public Query onlyDeleted() { + return withDeleted().whereNotNull(repo.getInfo().getSoftDeleteField()); + } + public void finalDelete() { - SQLQueryString qs = connection.builder().buildDelete(this); - try { - connection.write(qs.getQuery(), qs.getParameters().toArray()); - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); + try(PooledSQL connection = repo.getPool().get()) { + SQLQueryString qs = connection.builder().buildDelete(this); + try { + connection.write(qs.getQuery(), qs.getParameters().toArray()); + } catch (SQLException throwables) { + throw new ORMQueryException(throwables); + } } } @@ -391,18 +397,20 @@ public void restore() { } public T refresh(T entity) { - SQLQueryString qs = connection.builder().buildQuery(this); - try { - ResultSet rs = connection.read(qs.getQuery(), qs.getParameters().toArray()); - if(rs.next()) { - SQLMapper.mapBack(repo, rs, entity); - connection.close(rs); - return entity; - } else { - return null; + try(PooledSQL connection = repo.getPool().get()) { + SQLQueryString qs = connection.builder().buildQuery(this); + try { + ResultSet rs = connection.read(qs.getQuery(), qs.getParameters().toArray()); + if(rs.next()) { + SQLMapper.mapBack(repo, rs, entity); + connection.close(rs); + return entity; + } else { + return null; + } + } catch (SQLException throwables) { + throw new ORMQueryException(throwables); } - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); } } @@ -411,23 +419,27 @@ public void update(T entity) { } public void update(Map values) { - SQLQueryString queryString = connection.builder().buildUpdate(this, values); - try { - connection.write(queryString.getQuery(), queryString.getParameters().toArray()); - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); + try(PooledSQL connection = repo.getPool().get()) { + SQLQueryString queryString = connection.builder().buildUpdate(this, values); + try { + connection.write(queryString.getQuery(), queryString.getParameters().toArray()); + } catch (SQLException throwables) { + throw new ORMQueryException(throwables); + } } } public List all() { - SQLQueryString qs = connection.builder().buildQuery(this); - try { - ResultSet rs = connection.read(qs.getQuery(), qs.getParameters().toArray()); - List list = SQLMapper.map(repo, rs, new ArrayList<>()); - connection.close(rs); - return list; - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); + try(PooledSQL connection = repo.getPool().get()) { + SQLQueryString qs = connection.builder().buildQuery(this); + try { + ResultSet rs = connection.read(qs.getQuery(), qs.getParameters().toArray()); + List list = SQLMapper.map(repo, rs, new ArrayList<>()); + connection.close(rs); + return list; + } catch (SQLException throwables) { + throw new ORMQueryException(throwables); + } } } @@ -447,21 +459,32 @@ public Stream stream() { } public int count() { - SQLQueryString qs = connection.builder().buildQuery(this.select("count(*)")); - try { - ResultSet rs = connection.read(qs.getQuery(), qs.getParameters().toArray()); - int c = 0; - if (rs.next()) - c = rs.getInt(1); - connection.close(rs); - return c; - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); + try(PooledSQL connection = repo.getPool().get()) { + SQLQueryString qs = connection.builder().buildQuery(this.select("count(*)")); + try { + ResultSet rs = connection.read(qs.getQuery(), qs.getParameters().toArray()); + int c = 0; + if (rs.next()) + c = rs.getInt(1); + connection.close(rs); + return c; + } catch (SQLException throwables) { + throw new ORMQueryException(throwables); + } } } + @Deprecated public boolean hasRecords() { - return count() > 0; + return isNotEmpty(); + } + + public boolean isNotEmpty() { + return limit(1).count() > 0; + } + + public boolean isEmpty() { + return !isNotEmpty(); } } diff --git a/src/main/java/org/javawebstack/orm/query/QueryGroup.java b/src/main/java/org/javawebstack/orm/query/QueryGroup.java index 446b6f1..46c2288 100644 --- a/src/main/java/org/javawebstack/orm/query/QueryGroup.java +++ b/src/main/java/org/javawebstack/orm/query/QueryGroup.java @@ -6,8 +6,10 @@ import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.function.Function; +import java.util.stream.Stream; /** * Queries grouped via the QueryGroup class will be put inside parenthesis. @@ -37,9 +39,9 @@ public QueryGroup and(Function, QueryGroup> group) { public QueryGroup where(Function, QueryGroup> group) { QueryGroup innerGroup = group.apply(new QueryGroup<>()); - if(innerGroup.queryElements.size() == 0) + if(innerGroup.queryElements.isEmpty()) return this; - if (queryElements.size() > 0) + if (!queryElements.isEmpty()) queryElements.add(QueryConjunction.AND); queryElements.add(innerGroup); return this; @@ -52,9 +54,9 @@ public QueryGroup or(Function, QueryGroup> group) { public QueryGroup orWhere(Function, QueryGroup> group) { QueryGroup innerGroup = group.apply(new QueryGroup<>()); - if(innerGroup.queryElements.size() == 0) + if(innerGroup.queryElements.isEmpty()) return this; - if (queryElements.size() > 0) + if (!queryElements.isEmpty()) queryElements.add(QueryConjunction.OR); queryElements.add(innerGroup); return this; @@ -65,12 +67,22 @@ public QueryGroup where(Object left, String condition, Object right) { return whereNull(left); if(condition.equalsIgnoreCase("!=") && right == null) return whereNotNull(left); - if((condition.equalsIgnoreCase("IN") || condition.equalsIgnoreCase("NOT IN")) && (right == null || Array.getLength(right) == 0)) { - left = 1; - condition = "="; - right = 2; + + if(condition.equalsIgnoreCase("IN") || condition.equalsIgnoreCase("NOT IN")) { + Object[] values = (Object[]) right; + if (values != null && values.length == 1) { + if (values[0] instanceof Collection) + values = ((Collection) values[0]).toArray(); + else if (values[0] instanceof Stream) + values = ((Stream) values[0]).toArray(); + } + if(values == null || values.length == 0) { + left = 1; + condition = "="; + right = 2; + } } - if (queryElements.size() > 0) + if (!queryElements.isEmpty()) queryElements.add(QueryConjunction.AND); queryElements.add(new QueryCondition(left instanceof String ? new QueryColumn((String) left) : left, condition, right)); return this; @@ -120,7 +132,7 @@ public QueryGroup orWhere(Object left, String condition, Object right) { condition = "="; right = 2; } - if (queryElements.size() > 0) + if (!queryElements.isEmpty()) queryElements.add(QueryConjunction.OR); queryElements.add(new QueryCondition(left instanceof String ? new QueryColumn((String) left) : left, condition, right)); return this; @@ -173,7 +185,7 @@ public QueryGroup orWhereNotNull(Object left) { } public QueryGroup whereExists(Class model, Function, Query> consumer) { - if (queryElements.size() > 0) + if (!queryElements.isEmpty()) queryElements.add(QueryConjunction.AND); Query query = consumer.apply(new Query<>(model).limit(1)); queryElements.add(new QueryExists<>(query, false)); @@ -181,7 +193,7 @@ public QueryGroup whereExists(Class model, Function QueryGroup orWhereExists(Class model, Function, Query> consumer) { - if (queryElements.size() > 0) + if (!queryElements.isEmpty()) queryElements.add(QueryConjunction.OR); Query query = consumer.apply(new Query<>(model).limit(1)); queryElements.add(new QueryExists<>(query, false)); @@ -189,7 +201,7 @@ public QueryGroup orWhereExists(Class model, Function QueryGroup whereNotExists(Class model, Function, Query> consumer) { - if (queryElements.size() > 0) + if (!queryElements.isEmpty()) queryElements.add(QueryConjunction.AND); Query query = consumer.apply(new Query<>(model).limit(1)); queryElements.add(new QueryExists<>(query, true)); @@ -197,7 +209,7 @@ public QueryGroup whereNotExists(Class model, Function QueryGroup orWhereNotExists(Class model, Function, Query> consumer) { - if (queryElements.size() > 0) + if (!queryElements.isEmpty()) queryElements.add(QueryConjunction.OR); Query query = consumer.apply(new Query<>(model).limit(1)); queryElements.add(new QueryExists<>(query, true)); diff --git a/src/main/java/org/javawebstack/orm/wrapper/builder/MySQLQueryStringBuilder.java b/src/main/java/org/javawebstack/orm/renderer/MySQLQueryStringRenderer.java similarity index 96% rename from src/main/java/org/javawebstack/orm/wrapper/builder/MySQLQueryStringBuilder.java rename to src/main/java/org/javawebstack/orm/renderer/MySQLQueryStringRenderer.java index 3c706e1..44ee5c8 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/builder/MySQLQueryStringBuilder.java +++ b/src/main/java/org/javawebstack/orm/renderer/MySQLQueryStringRenderer.java @@ -1,4 +1,4 @@ -package org.javawebstack.orm.wrapper.builder; +package org.javawebstack.orm.renderer; import org.javawebstack.orm.*; import org.javawebstack.orm.exception.ORMQueryException; @@ -8,16 +8,13 @@ import java.lang.reflect.Method; import java.sql.Timestamp; import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; -public class MySQLQueryStringBuilder implements QueryStringBuilder { +public class MySQLQueryStringRenderer implements QueryStringRenderer { - public static final MySQLQueryStringBuilder INSTANCE = new MySQLQueryStringBuilder(); + public static final MySQLQueryStringRenderer INSTANCE = new MySQLQueryStringRenderer(); private static Method accessibleAccessMethod; @@ -63,6 +60,8 @@ public SQLQueryString buildQuery(Query query) { QueryGroup where = (QueryGroup) query.getWhereGroup(); checkWithDeleted(repo, query.isWithDeleted(), where); if(query.shouldApplyAccessible()) { + if(repo.getAccessible() == null) + throw new ORMQueryException("No accessible was set on the repository for " + repo.getInfo().getModelClass().getName()); QueryGroup accessChecks; try { accessChecks = (QueryGroup) accessibleAccessMethod.invoke(repo.getAccessible(), query, new QueryGroup<>(), query.getAccessor()); diff --git a/src/main/java/org/javawebstack/orm/wrapper/builder/QueryStringBuilder.java b/src/main/java/org/javawebstack/orm/renderer/QueryStringRenderer.java similarity index 80% rename from src/main/java/org/javawebstack/orm/wrapper/builder/QueryStringBuilder.java rename to src/main/java/org/javawebstack/orm/renderer/QueryStringRenderer.java index 0b48378..b0b61a5 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/builder/QueryStringBuilder.java +++ b/src/main/java/org/javawebstack/orm/renderer/QueryStringRenderer.java @@ -1,11 +1,11 @@ -package org.javawebstack.orm.wrapper.builder; +package org.javawebstack.orm.renderer; import org.javawebstack.orm.TableInfo; import org.javawebstack.orm.query.Query; import java.util.Map; -public interface QueryStringBuilder { +public interface QueryStringRenderer { SQLQueryString buildInsert(TableInfo info, Map values); SQLQueryString buildQuery(Query query); diff --git a/src/main/java/org/javawebstack/orm/wrapper/builder/SQLQueryString.java b/src/main/java/org/javawebstack/orm/renderer/SQLQueryString.java similarity index 93% rename from src/main/java/org/javawebstack/orm/wrapper/builder/SQLQueryString.java rename to src/main/java/org/javawebstack/orm/renderer/SQLQueryString.java index 3c60a42..1947c8a 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/builder/SQLQueryString.java +++ b/src/main/java/org/javawebstack/orm/renderer/SQLQueryString.java @@ -1,4 +1,4 @@ -package org.javawebstack.orm.wrapper.builder; +package org.javawebstack.orm.renderer; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/org/javawebstack/orm/wrapper/SQLDriverFactory.java b/src/main/java/org/javawebstack/orm/util/SQLDriverFactory.java similarity index 86% rename from src/main/java/org/javawebstack/orm/wrapper/SQLDriverFactory.java rename to src/main/java/org/javawebstack/orm/util/SQLDriverFactory.java index a07bb82..64a695a 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/SQLDriverFactory.java +++ b/src/main/java/org/javawebstack/orm/util/SQLDriverFactory.java @@ -1,4 +1,8 @@ -package org.javawebstack.orm.wrapper; +package org.javawebstack.orm.util; + +import org.javawebstack.orm.connection.MySQL; +import org.javawebstack.orm.connection.SQL; +import org.javawebstack.orm.connection.SQLite; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/org/javawebstack/orm/wrapper/SQLDriverNotFoundException.java b/src/main/java/org/javawebstack/orm/util/SQLDriverNotFoundException.java similarity index 87% rename from src/main/java/org/javawebstack/orm/wrapper/SQLDriverNotFoundException.java rename to src/main/java/org/javawebstack/orm/util/SQLDriverNotFoundException.java index 2466d78..789f35b 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/SQLDriverNotFoundException.java +++ b/src/main/java/org/javawebstack/orm/util/SQLDriverNotFoundException.java @@ -1,14 +1,15 @@ -package org.javawebstack.orm.wrapper; +package org.javawebstack.orm.util; public class SQLDriverNotFoundException extends Exception { + private String name; public SQLDriverNotFoundException (String name) { this.name = name; } - public String getMessage() { return "SQL Driver " + name + " not found!"; } + } diff --git a/src/test/java/org/javawebstack/orm/test/SQLDriverFactoryTest.java b/src/test/java/org/javawebstack/orm/test/SQLDriverFactoryTest.java index d6b1299..9438c16 100644 --- a/src/test/java/org/javawebstack/orm/test/SQLDriverFactoryTest.java +++ b/src/test/java/org/javawebstack/orm/test/SQLDriverFactoryTest.java @@ -1,8 +1,8 @@ package org.javawebstack.orm.test; -import org.javawebstack.orm.wrapper.MySQL; -import org.javawebstack.orm.wrapper.SQLDriverFactory; -import org.javawebstack.orm.wrapper.SQLDriverNotFoundException; +import org.javawebstack.orm.connection.MySQL; +import org.javawebstack.orm.util.SQLDriverFactory; +import org.javawebstack.orm.util.SQLDriverNotFoundException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/javawebstack/orm/test/UpdateOnlyIfIsDirtyTest.java b/src/test/java/org/javawebstack/orm/test/UpdateOnlyIfIsDirtyTest.java index 44076f2..e3edb18 100644 --- a/src/test/java/org/javawebstack/orm/test/UpdateOnlyIfIsDirtyTest.java +++ b/src/test/java/org/javawebstack/orm/test/UpdateOnlyIfIsDirtyTest.java @@ -4,7 +4,7 @@ import org.javawebstack.orm.ORMConfig; import org.javawebstack.orm.exception.ORMConfigurationException; import org.javawebstack.orm.test.shared.models.JustString; -import org.javawebstack.orm.wrapper.QueryLogger; +import org.javawebstack.orm.connection.QueryLogger; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/src/test/java/org/javawebstack/orm/test/automigrate/IndexTest.java b/src/test/java/org/javawebstack/orm/test/automigrate/IndexTest.java new file mode 100644 index 0000000..b86956e --- /dev/null +++ b/src/test/java/org/javawebstack/orm/test/automigrate/IndexTest.java @@ -0,0 +1,36 @@ +package org.javawebstack.orm.test.automigrate; + +import org.javawebstack.orm.ORM; +import org.javawebstack.orm.ORMConfig; +import org.javawebstack.orm.exception.ORMConfigurationException; +import org.javawebstack.orm.test.ORMTestCase; +import org.javawebstack.orm.test.shared.models.IndexType; +import org.javawebstack.orm.test.shared.verification.Indexes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; + +public class IndexTest extends ORMTestCase { + + @BeforeEach + public void setUp() throws ORMConfigurationException { + ORMConfig config = new ORMConfig() + .setDefaultSize(255); + ORM.register(IndexType.class, sql(), config); + ORM.autoMigrate(true); + } + + @Test + public void hasIndexes() throws SQLException { + Indexes indexes = new Indexes("index_types"); + indexes.assertHasIndex("idx_key_value"); // auto generated + indexes.assertHasIndex("idx_custom"); // explicit set + } + + @Test + public void testUnique() throws SQLException { + Indexes indexes = new Indexes("index_types"); + indexes.assertIsUnique("idx_key"); + } +} diff --git a/src/test/java/org/javawebstack/orm/test/querybuilding/FromClauseTest.java b/src/test/java/org/javawebstack/orm/test/querybuilding/FromClauseTest.java index 227d733..cc51855 100644 --- a/src/test/java/org/javawebstack/orm/test/querybuilding/FromClauseTest.java +++ b/src/test/java/org/javawebstack/orm/test/querybuilding/FromClauseTest.java @@ -3,6 +3,7 @@ import org.javawebstack.orm.Model; import org.javawebstack.orm.ORM; import org.javawebstack.orm.Repo; +import org.javawebstack.orm.connection.pool.PooledSQL; import org.javawebstack.orm.exception.ORMConfigurationException; import org.javawebstack.orm.test.ORMTestCase; import org.javawebstack.orm.test.shared.models.tablenames.*; @@ -68,6 +69,8 @@ void testOverwrittenTableName() throws ORMConfigurationException { */ private String getBaseQuery(Class clazz) throws ORMConfigurationException { ORM.register(clazz, sql()); - return Repo.get(clazz).getConnection().builder().buildQuery(Repo.get(clazz).query()).getQuery(); + try(PooledSQL sql = Repo.get(clazz).getPool().get()) { + return sql.builder().buildQuery(Repo.get(clazz).query()).getQuery(); + } } } diff --git a/src/test/java/org/javawebstack/orm/test/shared/models/IndexType.java b/src/test/java/org/javawebstack/orm/test/shared/models/IndexType.java new file mode 100644 index 0000000..e9a5085 --- /dev/null +++ b/src/test/java/org/javawebstack/orm/test/shared/models/IndexType.java @@ -0,0 +1,19 @@ +package org.javawebstack.orm.test.shared.models; + +import org.javawebstack.orm.Model; +import org.javawebstack.orm.annotation.Column; +import org.javawebstack.orm.annotation.Index; + +import java.util.UUID; + +@Index({"key", "value"}) +@Index(value = {"id", "key"}, id = "idx_custom") +@Index(value = {"key"}, unique = true) +public class IndexType extends Model { + @Column + UUID id; + @Column + String key; + @Column + String value; +} diff --git a/src/test/java/org/javawebstack/orm/test/shared/settings/MySQLConnectionContainer.java b/src/test/java/org/javawebstack/orm/test/shared/settings/MySQLConnectionContainer.java index c8472d9..e9a0fbe 100644 --- a/src/test/java/org/javawebstack/orm/test/shared/settings/MySQLConnectionContainer.java +++ b/src/test/java/org/javawebstack/orm/test/shared/settings/MySQLConnectionContainer.java @@ -1,7 +1,7 @@ package org.javawebstack.orm.test.shared.settings; -import org.javawebstack.orm.wrapper.MySQL; -import org.javawebstack.orm.wrapper.SQL; +import org.javawebstack.orm.connection.MySQL; +import org.javawebstack.orm.connection.SQL; import java.io.File; import java.io.FileInputStream; diff --git a/src/test/java/org/javawebstack/orm/test/shared/verification/Indexes.java b/src/test/java/org/javawebstack/orm/test/shared/verification/Indexes.java new file mode 100644 index 0000000..3ca806e --- /dev/null +++ b/src/test/java/org/javawebstack/orm/test/shared/verification/Indexes.java @@ -0,0 +1,55 @@ +package org.javawebstack.orm.test.shared.verification; + +import lombok.Builder; +import lombok.ToString; +import org.javawebstack.orm.test.shared.settings.MySQLConnectionContainer; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class Indexes extends MySQLConnectionContainer { + String tableName; + List indices = new ArrayList<>(); + + public Indexes(String tableName) throws SQLException { + this.tableName = tableName; + String query = String.format("SHOW INDEXES FROM %s WHERE `key_name` != 'PRIMARY'", tableName); + ResultSet resultSet = sql().read(query); + while (resultSet.next()) { + indices.add(IndexInfo.builder() + .name(resultSet.getString("key_name")) + .type(resultSet.getString("index_type")) + .unique(resultSet.getInt("non_unique") != 1) + .build() + ); + } + } + + public void assertHasIndex(String indexName) { + assertTrue( + indices.stream().anyMatch(i -> i.name.equals(indexName)), + String.format("Index %s.%s doesn't exist on table.", tableName, indexName) + ); + } + + public void assertIsUnique(String indexName) { + IndexInfo indexInfo = indices.stream().filter(i -> i.name.equals(indexName)).findFirst().orElse(null); + assertNotNull(indexInfo); + assertTrue( + indexInfo.unique, + String.format("Index %s.%s is not unique.", tableName, indexInfo) + ); + } + + @Builder + @ToString + public static class IndexInfo { + String name; + String type; + boolean unique; + } +} diff --git a/src/test/java/org/javawebstack/orm/test/shared/verification/QueryVerification.java b/src/test/java/org/javawebstack/orm/test/shared/verification/QueryVerification.java index 3911d99..6ace800 100644 --- a/src/test/java/org/javawebstack/orm/test/shared/verification/QueryVerification.java +++ b/src/test/java/org/javawebstack/orm/test/shared/verification/QueryVerification.java @@ -1,5 +1,6 @@ package org.javawebstack.orm.test.shared.verification; +import org.javawebstack.orm.connection.pool.PooledSQL; import org.javawebstack.orm.query.Query; import org.javawebstack.orm.test.exception.SectionIndexOutOfBoundException; import org.javawebstack.orm.test.shared.util.QueryStringUtil; @@ -145,8 +146,10 @@ public String getSection(String topLevelKeyword, int sectionIndex) throws Sectio * @return The order sensitive string list of inner sections. */ public List getSectionList(String topLevelKeyword) { - return new QueryStringUtil(this.query.getRepo().getConnection().builder().buildQuery(this.query).getQuery()) - .getTopLevelSectionsByKeyword(topLevelKeyword); + try(PooledSQL sql = this.query.getRepo().getPool().get()) { + return new QueryStringUtil(sql.builder().buildQuery(this.query).getQuery()) + .getTopLevelSectionsByKeyword(topLevelKeyword); + } } private void failDueToSectionIndexOutOfBounds(SectionIndexOutOfBoundException exception) {