diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8c73d6e..3a5558214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,11 @@ The pull request for this change is ([#550](https://github.com/mybatis/mybatis-d 3. Refactored the common insert mapper support for MyBatis3 by adding a CommonGeneralInsertMapper that can be used without a class that matches the table row. It includes methods for general insert and insert select. ([#570](https://github.com/mybatis/mybatis-dynamic-sql/pull/570)) +4. Added the ability to change a table name on AliasableSqlTable - this creates a new instance of the object with a new + name. This is useful in sharded databases where the name of the table is calculated based on some sharding + algorithm. Also deprecated the constructors on SqlTable that accept Suppliers for table name - this creates an + effectively mutable object and goes against the principles of immutability that we strive for in the library. + ([#572](https://github.com/mybatis/mybatis-dynamic-sql/pull/572)) diff --git a/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java b/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java index 27c0fbb3f..ab1a44b2f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java +++ b/src/main/java/org/mybatis/dynamic/sql/AliasableSqlTable.java @@ -29,14 +29,26 @@ protected AliasableSqlTable(String tableName, Supplier constructor) { this.constructor = Objects.requireNonNull(constructor); } - protected AliasableSqlTable(Supplier tableNameSupplier, Supplier constructor) { - super(tableNameSupplier); - this.constructor = Objects.requireNonNull(constructor); - } - public T withAlias(String alias) { T newTable = constructor.get(); ((AliasableSqlTable) newTable).tableAlias = alias; + newTable.nameSupplier = nameSupplier; + return newTable; + } + + /** + * Returns a new instance of this table with the specified name. All column instances are recreated. + * This is useful for sharding where the table name may change at runtime based on some sharding algorithm, + * but all other table attributes are the same. + * + * @param name new name for the table + * @return a new AliasableSqlTable with the specified name, all other table attributes are copied + */ + public T withName(String name) { + Objects.requireNonNull(name); + T newTable = constructor.get(); + ((AliasableSqlTable) newTable).tableAlias = tableAlias; + newTable.nameSupplier = () -> name; return newTable; } diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlTable.java b/src/main/java/org/mybatis/dynamic/sql/SqlTable.java index 9ddc3e84c..b5f03aea9 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlTable.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlTable.java @@ -24,7 +24,7 @@ public class SqlTable implements TableExpression { - private final Supplier nameSupplier; + protected Supplier nameSupplier; protected SqlTable(String tableName) { Objects.requireNonNull(tableName); @@ -32,16 +32,40 @@ protected SqlTable(String tableName) { this.nameSupplier = () -> tableName; } + /** + * Creates an SqlTable whose name can be changed at runtime. + * + * @param tableNameSupplier table name supplier + * @deprecated please use {@link AliasableSqlTable} if you need to change the table name at runtime + */ + @Deprecated protected SqlTable(Supplier tableNameSupplier) { Objects.requireNonNull(tableNameSupplier); this.nameSupplier = tableNameSupplier; } + /** + * Creates an SqlTable whose name can be changed at runtime. + * + * @param schemaSupplier schema supplier + * @param tableName table name + * @deprecated please use {@link AliasableSqlTable} if you need to change the table name at runtime + */ + @Deprecated protected SqlTable(Supplier> schemaSupplier, String tableName) { this(Optional::empty, schemaSupplier, tableName); } + /** + * Creates an SqlTable whose name can be changed at runtime. + * + * @param catalogSupplier catalog supplier + * @param schemaSupplier schema supplier + * @param tableName table name + * @deprecated please use {@link AliasableSqlTable} if you need to change the table name at runtime + */ + @Deprecated protected SqlTable(Supplier> catalogSupplier, Supplier> schemaSupplier, String tableName) { Objects.requireNonNull(catalogSupplier); diff --git a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java index 80c07780c..93659a13b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java @@ -561,7 +561,8 @@ public QueryExpressionHavingBuilder having(SqlCriterion initialCriterion, AndOrC return having(initialCriterion, Arrays.asList(subCriteria)); } - public QueryExpressionHavingBuilder having(SqlCriterion initialCriterion, List subCriteria) { + public QueryExpressionHavingBuilder having(SqlCriterion initialCriterion, + List subCriteria) { return QueryExpressionDSL.this.having(initialCriterion, subCriteria); } } @@ -599,7 +600,8 @@ public FromGatherer selectDistinct(List selectList) { } } - public class QueryExpressionHavingBuilder extends AbstractBooleanExpressionDSL implements Buildable { + public class QueryExpressionHavingBuilder extends AbstractBooleanExpressionDSL + implements Buildable { public QueryExpressionHavingBuilder(SqlCriterion initialCriterion) { setInitialCriterion(initialCriterion); diff --git a/src/site/markdown/docs/databaseObjects.md b/src/site/markdown/docs/databaseObjects.md index e71e56540..a54be5414 100644 --- a/src/site/markdown/docs/databaseObjects.md +++ b/src/site/markdown/docs/databaseObjects.md @@ -4,14 +4,16 @@ MyBatis Dynamic SQL works with Java objects that represent relational tables or ## Table or View Representation The class `org.mybatis.dynamic.sql.SqlTable` is used to represent a table or view in a database. An `SqlTable` holds a -name, and a collection of `SqlColumn` objects that represent the columns in a table or view. +name, and a collection of `SqlColumn` objects that represent the columns in a table or view. A subclass of `SqlTable` - +`AliasableSqlTable` should be used in cases where you want to specify a table alias that should be used in all cases, +or if you need to change the table name at runtime. A table or view name in SQL has three parts: 1. The catalog - which is optional and is rarely used outside of Microsoft SQL Server. If unspecified the default catalog will be used - and many databases only have one catalog -1. The schema - which is optional but is very often specified. If unspecified, the default schema will be used -1. The table name - which is required +2. The schema - which is optional but is very often specified. If unspecified, the default schema will be used +3. The table name - which is required Typical examples of names are as follows: @@ -21,20 +23,21 @@ Typical examples of names are as follows: - `"bar"` - a name with just a table name (bar). This will access a table or view in the default catalog and schema for a connection -In MyBatis Dynamic SQL, the table or view name can be specified in different ways: +In MyBatis Dynamic SQL, the full name of the table should be supplied on the constructor of the table object. +If a table name needs to change at runtime (say for sharding support), then use the `withName` method on +`AliasableSqlTable` to create an instance with the new name. -1. The name can be a constant String -1. The name can be calculated at runtime based on a catalog and/or schema supplier functions and a constant table name -1. The name can be calculated at runtime with a name supplier function +We recommend using the base class `AliasableSqlTable` in all cases as it provides the most flexibility. The +`SqlTable` class remains in the library for compatibility with older code only. -### Constant Names - -Constant names are used when you use the `SqlTable` constructor with a single String argument. For example: +For example: ```java -public class MyTable extends SqlTable { +import org.mybatis.dynamic.sql.AliasableSqlTable; + +public class MyTable extends AliasableSqlTable { public MyTable() { - super("MyTable"); + super("MyTable", MyTable::new); } } ``` @@ -42,105 +45,26 @@ public class MyTable extends SqlTable { Or ```java -public class MyTable extends SqlTable { +public class MyTable extends AliasableSqlTable { public MyTable() { - super("MySchema.MyTable"); - } -} -``` - -### Dynamic Catalog and/or Schema Names -MyBatis Dynamic SQL allows you to dynamically specify a catalog and/or schema. This is useful for applications where -the schema may change for different users or environments, or if you are using different schemas to shard the database. -Dynamic names are used when you use a `SqlTable` constructor that accepts one or more `java.util.function.Supplier` -arguments. - -For example, suppose you wanted to change the schema based on the value of a system property. You could write a class -like this: - -```java -public class SchemaSupplier { - public static final String schema_property = "schemaToUse"; - - public static Optional schemaPropertyReader() { - return Optional.ofNullable(System.getProperty(schema_property)); + super("MySchema.MyTable", MyTable::new); } } ``` -This class has a static method `schemaPropertyReader` that will return an `Optional` containing the value of a -system property. You could then reference this method in the constructor of the `SqlTable` like this: - -```java -public static final class User extends SqlTable { - public User() { - super(SchemaSupplier::schemaPropertyReader, "User"); - } -} -``` - -Whenever the table is referenced for rendering SQL, the name will be calculated based on the current value of the -system property. - -There are two constructors that can be used for dynamic names: - -1. A constructor that accepts `Supplier>` for the schema, and `String` for the name. This constructor - assumes that the catalog is always empty or not used -1. A constructor that accepts `Supplier>` for the catalog, `Supplier>` for the schema, - and `String` for the name - -If you are using Microsoft SQL Server and want to use a dynamic catalog name and ignore the schema, then you should use -the second constructor like this: +You can change a table name: ```java -public static final class User extends SqlTable { - public User() { - super(CatalogSupplier::catalogPropertyReader, Optional::empty, "User"); - } -} -``` - -The following table shows how the name is calculated in all combinations of suppliers: - -Catalog Supplier Value | Schema Supplier Value | Name | Fully Qualified Name ----|---|---|--- -"MyCatalog" | "MySchema" | "MyTable" | "MyCatalog.MySchema.MyTable" -<empty> | "MySchema" | "MyTable" | "MySchema.MyTable" -"MyCatalog" | <empty> | "MyTable" | "MyCatalog..MyTable" -<empty> | <empty> | "MyTable" | "MyTable" - - -### Fully Dynamic Names -MyBatis Dynamic SQL allows you to dynamically specify a full table name. This is useful for applications where the -database is sharded with different tables representing different shards of the whole. Dynamic names are used when you -use a `SqlTable` constructor that accepts a single `java.util.function.Supplier` argument. - -For example, suppose you wanted to change the name based on the value of a system property. You could write a class -like this: - -```java -public class NameSupplier { - public static final String name_property = "nameToUse"; - - public static String namePropertyReader() { - return System.getProperty(name_property); +public class MyTable extends AliasableSqlTable { + public MyTable() { + super("Schema1.MyTable", MyTable::new); } } -``` - -This class has a static method `namePropertyReader` that will return an `String` containing the value of a system -property. You could then reference this method in the constructor of the `SqlTable` like this: -```java -public static final class User extends SqlTable { - public User() { - super(NameSupplier::namePropertyReader); - } -} +MyTable schema1Table = new MyTable(); +MyTable schema2Table = schema1Table.withName("Schema2.MyTable"); ``` -Whenever the table is referenced for rendering SQL, the name will be calculated based on the current value of the system property. - ## Aliased Tables In join queries, it is usually a good practice to specify table aliases. The `select` statement includes diff --git a/src/test/java/examples/joins/UserDynamicSQLSupport.java b/src/test/java/examples/joins/UserDynamicSQLSupport.java index 6a8f2c9a0..4f4782d56 100644 --- a/src/test/java/examples/joins/UserDynamicSQLSupport.java +++ b/src/test/java/examples/joins/UserDynamicSQLSupport.java @@ -32,7 +32,7 @@ public static final class User extends AliasableSqlTable { public final SqlColumn parentId = column("parent_id", JDBCType.INTEGER); public User() { - super(() -> "User", User::new); + super("User", User::new); } } } diff --git a/src/test/java/examples/sharding/ShardedMapper.java b/src/test/java/examples/sharding/ShardedMapper.java new file mode 100644 index 000000000..106d10201 --- /dev/null +++ b/src/test/java/examples/sharding/ShardedMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.sharding; + +import org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper; +import org.mybatis.dynamic.sql.util.mybatis3.CommonGeneralInsertMapper; +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; + +public interface ShardedMapper extends CommonCountMapper, CommonGeneralInsertMapper, CommonSelectMapper { +} diff --git a/src/test/java/examples/sharding/ShardingTest.java b/src/test/java/examples/sharding/ShardingTest.java new file mode 100644 index 000000000..597c227c0 --- /dev/null +++ b/src/test/java/examples/sharding/ShardingTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2016-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.sharding; + +import static examples.sharding.TableCodesDynamicSqlSupport.tableCodes; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mybatis.dynamic.sql.SqlBuilder.countFrom; +import static org.mybatis.dynamic.sql.SqlBuilder.insertInto; +import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; +import static org.mybatis.dynamic.sql.SqlBuilder.select; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.sql.Connection; +import java.sql.DriverManager; +import java.util.HashMap; +import java.util.Map; + +import examples.sharding.TableCodesDynamicSqlSupport.TableCodes; +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.jdbc.ScriptRunner; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider; +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; + +class ShardingTest { + private static final String JDBC_URL = "jdbc:hsqldb:mem:aname"; + private static final String JDBC_DRIVER = "org.hsqldb.jdbcDriver"; + private final Map shards = new HashMap<>(); + + private SqlSessionFactory sqlSessionFactory; + + + @BeforeEach + void setup() throws Exception { + Class.forName(JDBC_DRIVER); + InputStream is = getClass().getResourceAsStream("/examples/sharding/ShardingDB.sql"); + assert is != null; + try (Connection connection = DriverManager.getConnection(JDBC_URL, "sa", "")) { + ScriptRunner sr = new ScriptRunner(connection); + sr.setLogWriter(null); + sr.runScript(new InputStreamReader(is)); + } + + UnpooledDataSource ds = new UnpooledDataSource(JDBC_DRIVER, JDBC_URL, "sa", ""); + Environment environment = new Environment("test", new JdbcTransactionFactory(), ds); + Configuration config = new Configuration(environment); + config.addMapper(ShardedMapper.class); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(config); + } + + @Test + void testShardedSelect() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + ShardedMapper mapper = sqlSession.getMapper(ShardedMapper.class); + TableCodes table = calculateTable(1); + + SelectStatementProvider selectStatement = select(table.description) + .from(table) + .where(table.id, isEqualTo(1)) + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select description from tableCodes_odd where id = #{parameters.p1,jdbcType=INTEGER}"); + + String description = mapper.selectOneString(selectStatement); + assertThat(description).isNull(); + } + } + + @Test + void testShardedInserts() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + ShardedMapper mapper = sqlSession.getMapper(ShardedMapper.class); + + mapper.generalInsert(buildInsert(1, "Description 1")); + mapper.generalInsert(buildInsert(2, "Description 2")); + mapper.generalInsert(buildInsert(3, "Description 3")); + mapper.generalInsert(buildInsert(4, "Description 4")); + mapper.generalInsert(buildInsert(5, "Description 5")); + mapper.generalInsert(buildInsert(6, "Description 6")); + mapper.generalInsert(buildInsert(7, "Description 7")); + + TableCodes oddTable = calculateTable(1); + SelectStatementProvider oddCountStatement = countFrom(oddTable) + .build() + .render(RenderingStrategies.MYBATIS3); + assertThat(oddCountStatement.getSelectStatement()).isEqualTo("select count(*) from tableCodes_odd"); + long oddRows = mapper.count(oddCountStatement); + assertThat(oddRows).isEqualTo(4L); + + TableCodes evenTable = calculateTable(2); + SelectStatementProvider evenCountStatement = countFrom(evenTable) + .build() + .render(RenderingStrategies.MYBATIS3); + assertThat(evenCountStatement.getSelectStatement()).isEqualTo("select count(*) from tableCodes_even"); + long evenRows = mapper.count(evenCountStatement); + assertThat(evenRows).isEqualTo(3L); + } + } + + @Test + void testShardedSelects() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + ShardedMapper mapper = sqlSession.getMapper(ShardedMapper.class); + + mapper.generalInsert(buildInsert(1, "Description 1")); + mapper.generalInsert(buildInsert(2, "Description 2")); + + assertThat(mapper.selectOneString(buildSelect(1))).isEqualTo("Description 1"); + assertThat(mapper.selectOneString(buildSelect(2))).isEqualTo("Description 2"); + assertThat(mapper.selectOneString(buildSelect(3))).isNull(); + assertThat(mapper.selectOneString(buildSelect(4))).isNull(); + } + } + + private GeneralInsertStatementProvider buildInsert(int id, String description) { + TableCodesDynamicSqlSupport.TableCodes table = calculateTable(id); + return insertInto(table) + .set(table.id).toValue(id) + .set(table.description).toValue(description) + .build() + .render(RenderingStrategies.MYBATIS3); + } + + private SelectStatementProvider buildSelect(int id) { + TableCodesDynamicSqlSupport.TableCodes table = calculateTable(id); + return select(table.description) + .from(table) + .where(table.id, isEqualTo(id)) + .build() + .render(RenderingStrategies.MYBATIS3); + } + + private TableCodes calculateTable(int id) { + if (id % 2 == 0) { + return shards.computeIfAbsent("even", k -> tableCodes); // tableCodes_even is the default + } else { + return shards.computeIfAbsent("odd", k -> tableCodes.withName("tableCodes_odd")); + } + } +} diff --git a/src/test/java/examples/sharding/TableCodesDynamicSqlSupport.java b/src/test/java/examples/sharding/TableCodesDynamicSqlSupport.java new file mode 100644 index 000000000..c9dd2e72a --- /dev/null +++ b/src/test/java/examples/sharding/TableCodesDynamicSqlSupport.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.sharding; + +import org.mybatis.dynamic.sql.AliasableSqlTable; +import org.mybatis.dynamic.sql.SqlColumn; + +import java.sql.JDBCType; + +public final class TableCodesDynamicSqlSupport { + public static final TableCodes tableCodes = new TableCodes(); + public static final SqlColumn id = tableCodes.id; + public static final SqlColumn description = tableCodes.description; + + public static final class TableCodes extends AliasableSqlTable { + public final SqlColumn id = column("id", JDBCType.INTEGER); + public final SqlColumn description = column("description", JDBCType.VARCHAR); + public TableCodes() { + super("tableCodes_even", TableCodes::new); + } + } +} diff --git a/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java b/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java index 242ae8185..763c41aa5 100644 --- a/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java +++ b/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java @@ -47,6 +47,7 @@ import org.mybatis.dynamic.sql.update.UpdateDSL; import org.mybatis.dynamic.sql.update.UpdateModel; import org.mybatis.dynamic.sql.util.Messages; +import org.mybatis.dynamic.sql.where.condition.IsEqualTo; class InvalidSQLTest { @@ -253,8 +254,9 @@ void testInvalidMultipleWhereClauses() { .set(id).equalTo(3) .where(id, isEqualTo(2)); + IsEqualTo condition = isEqualTo(5); assertThatExceptionOfType(InvalidSqlException.class) - .isThrownBy(() -> builder.where(id, isEqualTo(5))) + .isThrownBy(() -> builder.where(id, condition)) .withMessage(Messages.getString("ERROR.32")); } diff --git a/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardedMapper.kt b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardedMapper.kt new file mode 100644 index 000000000..25a2c66ac --- /dev/null +++ b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardedMapper.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.kotlin.mybatis3.sharding + +import org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper +import org.mybatis.dynamic.sql.util.mybatis3.CommonGeneralInsertMapper +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper + +interface KShardedMapper: CommonCountMapper, CommonGeneralInsertMapper, CommonSelectMapper diff --git a/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardingTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardingTest.kt new file mode 100644 index 000000000..69abbca89 --- /dev/null +++ b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KShardingTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2016-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.kotlin.mybatis3.sharding + +import examples.kotlin.mybatis3.TestUtils +import examples.kotlin.mybatis3.sharding.KTableCodesTableDynamicSQLSupport.tableCodes +import org.apache.ibatis.session.SqlSessionFactory +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.countFrom +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertInto +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class KShardingTest { + private lateinit var sqlSessionFactory: SqlSessionFactory + private val shards = mutableMapOf() + + @BeforeAll + fun setup() { + sqlSessionFactory = TestUtils.buildSqlSessionFactory { + withInitializationScript("/examples/sharding/ShardingDB.sql") + withMapper(KShardedMapper::class) + } + } + + @Test + fun testShardedSelect() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(KShardedMapper::class.java) + val table = calculateTable(1) + + val selectStatement = select(table.description) { + from(table) + where { + table.id isEqualTo 1 + } + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select description from tableCodes_odd where id = #{parameters.p1,jdbcType=INTEGER}" + ) + + val description = mapper.selectOneString(selectStatement) + + assertThat(description).isNull() + } + } + + @Test + fun testShardedInserts() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(KShardedMapper::class.java) + + mapper.insert(1, "Description 1") + mapper.insert(2, "Description 2") + mapper.insert(3, "Description 3") + mapper.insert(4, "Description 4") + mapper.insert(5, "Description 5") + mapper.insert(6, "Description 6") + mapper.insert(7, "Description 7") + + val oddTable = calculateTable(1) + val oddCountStatement = countFrom(oddTable) { + allRows() + } + + assertThat(oddCountStatement.selectStatement).isEqualTo("select count(*) from tableCodes_odd") + val oddRows = mapper.count(oddCountStatement) + assertThat(oddRows).isEqualTo(4L) + + val evenTable = calculateTable(2) + val evenCountStatement = countFrom(evenTable) { + allRows() + } + + assertThat(evenCountStatement.selectStatement).isEqualTo("select count(*) from tableCodes_even") + val evenRows = mapper.count(evenCountStatement) + assertThat(evenRows).isEqualTo(3L) + } + } + + @Test + fun testShardedSelects() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(KShardedMapper::class.java) + + mapper.insert(1, "Description 1") + mapper.insert(2, "Description 2") + + assertThat(mapper.select(1)).isEqualTo("Description 1") + assertThat(mapper.select(2)).isEqualTo("Description 2") + assertThat(mapper.select(3)).isNull() + assertThat(mapper.select(4)).isNull() + } + } + + fun KShardedMapper.insert(id: Int, description: String): Int { + val table = calculateTable(id) + val insertStatement = insertInto(table) { + set(table.id) toValue id + set(table.description) toValue description + } + + return generalInsert(insertStatement) + } + + fun KShardedMapper.select(id: Int): String? { + val table = calculateTable(id) + val selectStatement = select(table.description) { + from(table) + where { + table.id isEqualTo id + } + } + + return selectOneString(selectStatement) + } + + private fun calculateTable(id: Int) = + if (id % 2 == 0) { + shards.computeIfAbsent("even") { tableCodes } // tableCodes_even is default + } else { + shards.computeIfAbsent("odd") { tableCodes.withName("tableCodes_odd") } + } +} diff --git a/src/test/kotlin/examples/kotlin/mybatis3/sharding/KTableCodesTableDynamicSQLSupport.kt b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KTableCodesTableDynamicSQLSupport.kt new file mode 100644 index 000000000..b4d3054a0 --- /dev/null +++ b/src/test/kotlin/examples/kotlin/mybatis3/sharding/KTableCodesTableDynamicSQLSupport.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.kotlin.mybatis3.sharding + +import org.mybatis.dynamic.sql.AliasableSqlTable +import java.sql.JDBCType +import org.mybatis.dynamic.sql.util.kotlin.elements.column + +object KTableCodesTableDynamicSQLSupport { + val tableCodes = TableCodes() + val id = tableCodes.id + val description = tableCodes.description + + class TableCodes : AliasableSqlTable("tableCodes_even", ::TableCodes) { + val id = column(name = "id", jdbcType = JDBCType.INTEGER) + val description = column(name = "description", jdbcType = JDBCType.VARCHAR) + } +} diff --git a/src/test/resources/examples/sharding/ShardingDB.sql b/src/test/resources/examples/sharding/ShardingDB.sql new file mode 100644 index 000000000..a9f8f350e --- /dev/null +++ b/src/test/resources/examples/sharding/ShardingDB.sql @@ -0,0 +1,30 @@ +-- +-- Copyright 2016-2022 the original author or authors. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +drop table tableCodes_even if exists; +drop table tableCodes_odd if exists; + +create table tableCodes_even ( + id int not null, + description varchar(30) not null, + primary key (id) +); + +create table tableCodes_odd ( + id int not null, + description varchar(30) not null, + primary key (id) +);