diff --git a/jdbc/mybatis/README.adoc b/jdbc/mybatis/README.adoc new file mode 100644 index 00000000..131224d1 --- /dev/null +++ b/jdbc/mybatis/README.adoc @@ -0,0 +1,10 @@ +== Spring Data JDBC mybatis + +`MyBatisTests` demonstrates how some queries executed by Spring Data JDBC can be replaced with MyBatis mappings. +The domain model is based on the basic JDBC example for comparison. + +The map of models is maintained by two statements configured in MyBatis mappings. + +`example.springdata.jdbc.mybatis.LegoSetMapper.findAllByProperty-models` showcases how a map can be loaded by configuring the select to return instances of `Map.Entry` + +`example.springdata.jdbc.mybatis.Model.insert` showcases how one can access the `MyBatixContext` and thereby the instance to save and the key of the parent entity. diff --git a/jdbc/mybatis/pom.xml b/jdbc/mybatis/pom.xml new file mode 100644 index 00000000..0ff45643 --- /dev/null +++ b/jdbc/mybatis/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + spring-data-jdbc-mybatis + + + org.springframework.data.examples + spring-data-jdbc-examples + 2.0.0.BUILD-SNAPSHOT + ../pom.xml + + + Spring Data JDBC - Using MyBatis for defining queries + Sample project demonstrating Spring Data JDBC features + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 1.3.1 + + + org.mybatis.spring.boot + mybatis-spring-boot-starter-test + 1.3.1 + test + + + + \ No newline at end of file diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/AgeGroup.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/AgeGroup.java new file mode 100644 index 00000000..a1e2d889 --- /dev/null +++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/AgeGroup.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2018 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 + * + * http://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 example.springdata.jdbc.mybatis; + +/** + * Age group for which a {@link LegoSet} is intended. + * + * @author Jens Schauder + */ +public enum AgeGroup { + + _0to3, _3to8, _8to12, _12andOlder +} diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSet.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSet.java new file mode 100644 index 00000000..6128e046 --- /dev/null +++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSet.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2018 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 + * + * http://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 example.springdata.jdbc.mybatis; + +import lombok.Data; + +import java.time.Period; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.annotation.Id; + +/** + * A Lego Set consisting of multiple Blocks and a manual + * + * @author Jens Schauder + */ +@Data +public class LegoSet { + + // You can build multiple models from one LegoSet + private final Map models = new HashMap<>(); + @Id private Integer id; + private String name; + private Manual manual; + + private static int toInt(Period period) { + return (int) (period == null ? 0 : period.get(ChronoUnit.YEARS)); + } + + private static Period toPeriod(int years) { + return Period.ofYears(years); + } + + public void addModel(String name, String description) { + + Model model = new Model(); + model.name = name; + model.description = description; + models.put(name, model); + } +} diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSetRepository.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSetRepository.java new file mode 100644 index 00000000..356ca1bf --- /dev/null +++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSetRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2018 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 + * + * http://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 example.springdata.jdbc.mybatis; + +import org.springframework.data.repository.CrudRepository; + +/** + * A repository for {@link LegoSet}. + * + * @author Jens Schauder + */ +public interface LegoSetRepository extends CrudRepository { + +} diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Manual.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Manual.java new file mode 100644 index 00000000..dee2fd32 --- /dev/null +++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Manual.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2018 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 + * + * http://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 example.springdata.jdbc.mybatis; + +import lombok.Data; + +import org.springframework.data.annotation.Id; + +/** + * A manual instructing how to assemble a {@link LegoSet}. + * + * @author Jens Schauder + */ +@Data +public class Manual { + + @Id private Long id; + private String author; + private String text; + + Manual(String text, String author) { + + this.id = null; + this.author = author; + this.text = text; + } +} diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Model.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Model.java new file mode 100644 index 00000000..b7e8f409 --- /dev/null +++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Model.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2018 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 + * + * http://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 example.springdata.jdbc.mybatis; + +import lombok.ToString; + +import org.springframework.data.domain.Persistable; +import org.springframework.lang.Nullable; + +/** + * One of potentially multiple models that can be build from a single {@link LegoSet}. + *

+ * No getters or setters needed. + * + * @author Jens Schauder + */ +@ToString +public class Model implements Persistable { + + String name; + + String description; + + @Nullable + @Override + public String getId() { + return name; + } + + @Override + public boolean isNew() { + return true; + } +} diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/ModelMapEntry.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/ModelMapEntry.java new file mode 100644 index 00000000..da61ea98 --- /dev/null +++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/ModelMapEntry.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2018 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 + * + * http://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 example.springdata.jdbc.mybatis; + +import java.sql.Clob; +import java.sql.SQLException; +import java.util.Map; + +/** + * @author Jens Schauder + */ +public class ModelMapEntry implements Map.Entry { + + private final String key; + private final Model value; + + ModelMapEntry(String name, Clob description) { + + key = name; + value = new Model(); + value.name = name; + try { + value.description = description.getSubString(1, (int) description.length()); + } catch (SQLException se) { + throw new RuntimeException(se); + } + } + + @Override + public String getKey() { + return key; + } + + @Override + public Model getValue() { + return value; + } + + @Override + public Model setValue(Model value) { + throw new UnsupportedOperationException("can't set the value of a ModelMapEntry"); + } +} diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/MyBatisConfiguration.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/MyBatisConfiguration.java new file mode 100644 index 00000000..6326847a --- /dev/null +++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/MyBatisConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2018 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 + * + * http://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 example.springdata.jdbc.mybatis; + +import static java.util.Arrays.*; + +import javax.sql.DataSource; + +import org.apache.ibatis.session.SqlSession; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jdbc.core.CascadingDataAccessStrategy; +import org.springframework.data.jdbc.core.DataAccessStrategy; +import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy; +import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy; +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +/** + * @author Jens Schauder + */ +@Configuration +@EnableJdbcRepositories +public class MyBatisConfiguration { + + // temporary workaround for https://jira.spring.io/browse/DATAJDBC-155 + @Bean + DataAccessStrategy defaultDataAccessStrategy(JdbcMappingContext context, DataSource dataSource, + SqlSession sqlSession) { + + NamedParameterJdbcOperations operations = new NamedParameterJdbcTemplate(dataSource); + + DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy(); + MyBatisDataAccessStrategy myBatisDataAccessStrategy = new MyBatisDataAccessStrategy(sqlSession); + + CascadingDataAccessStrategy cascadingDataAccessStrategy = new CascadingDataAccessStrategy( + asList(myBatisDataAccessStrategy, delegatingDataAccessStrategy)); + + DefaultDataAccessStrategy defaultDataAccessStrategy = new DefaultDataAccessStrategy( // + new SqlGeneratorSource(context), // + operations, // + context, // + cascadingDataAccessStrategy); + + delegatingDataAccessStrategy.setDelegate(defaultDataAccessStrategy); + + return cascadingDataAccessStrategy; + } +} diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Output.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Output.java new file mode 100644 index 00000000..7459f3f9 --- /dev/null +++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Output.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2018 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 + * + * http://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 example.springdata.jdbc.mybatis; + +import lombok.experimental.UtilityClass; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Trivial class to print domain objects to the console in a somewhat readable format. + * + * @author Jens Schauder + */ +@UtilityClass +public class Output { + + private final Logger LOG = LoggerFactory.getLogger(Output.class); + + public static void list(Iterable categories, String title) { + + StringBuilder message = new StringBuilder(String.format("==== %s ====\n", title)); + + categories.forEach(category -> { + message.append(category.toString().replace(", ", ",\n\t")); + }); + + LOG.info(message.toString()); + } +} diff --git a/jdbc/mybatis/src/main/resources/application.properties b/jdbc/mybatis/src/main/resources/application.properties new file mode 100644 index 00000000..1fce1e07 --- /dev/null +++ b/jdbc/mybatis/src/main/resources/application.properties @@ -0,0 +1,4 @@ +logging.level.org.springframework.data=INFO +logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG +logging.level.example.springdata.jdbc.mybatis=TRACE +mybatis.config-location=mybatis-config.xml \ No newline at end of file diff --git a/jdbc/mybatis/src/main/resources/example/springdata/jdbc/mybatis/LegoSet.xml b/jdbc/mybatis/src/main/resources/example/springdata/jdbc/mybatis/LegoSet.xml new file mode 100644 index 00000000..b174a89e --- /dev/null +++ b/jdbc/mybatis/src/main/resources/example/springdata/jdbc/mybatis/LegoSet.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/jdbc/mybatis/src/main/resources/example/springdata/jdbc/mybatis/Model.xml b/jdbc/mybatis/src/main/resources/example/springdata/jdbc/mybatis/Model.xml new file mode 100644 index 00000000..66cc3c4b --- /dev/null +++ b/jdbc/mybatis/src/main/resources/example/springdata/jdbc/mybatis/Model.xml @@ -0,0 +1,11 @@ + + + + + + + INSERT INTO Model (name, description, legoset) VALUES (#{instance.name}, #{instance.description}, #{parentId}) + + \ No newline at end of file diff --git a/jdbc/mybatis/src/main/resources/mybatis-config.xml b/jdbc/mybatis/src/main/resources/mybatis-config.xml new file mode 100644 index 00000000..ec0c130f --- /dev/null +++ b/jdbc/mybatis/src/main/resources/mybatis-config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/jdbc/mybatis/src/main/resources/schema.sql b/jdbc/mybatis/src/main/resources/schema.sql new file mode 100644 index 00000000..586ef36c --- /dev/null +++ b/jdbc/mybatis/src/main/resources/schema.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS LegoSet ( + id INTEGER IDENTITY PRIMARY KEY, + name VARCHAR(100) +); +CREATE TABLE IF NOT EXISTS manual ( + id INTEGER IDENTITY PRIMARY KEY, + LegoSet INTEGER, + author CHAR(100), + text VARCHAR(1000) +); +CREATE TABLE IF NOT EXISTS Model ( + name VARCHAR(100), + description CLOB, + legoset INTEGER +); diff --git a/jdbc/mybatis/src/test/java/example/springdata/jdbc/mybatis/MyBatisTests.java b/jdbc/mybatis/src/test/java/example/springdata/jdbc/mybatis/MyBatisTests.java new file mode 100644 index 00000000..f3f402ec --- /dev/null +++ b/jdbc/mybatis/src/test/java/example/springdata/jdbc/mybatis/MyBatisTests.java @@ -0,0 +1,73 @@ +package example.springdata.jdbc.mybatis; +/* + * Copyright 2017-2018 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 + * + * http://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. + */ + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mybatis.spring.boot.test.autoconfigure.MybatisTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureJdbc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Demonstrates queries can be mapped using MyBatis. + * + * @author Jens Schauder + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = MyBatisConfiguration.class) +@AutoConfigureJdbc +@MybatisTest +public class MyBatisTests { + + @Autowired private LegoSetRepository repository; + + @Test + public void exerciseSomewhatComplexEntity() { + + LegoSet smallCar = createLegoSet(); + smallCar.setManual(new Manual("Just put all the pieces together in the right order", "Jens Schauder")); + smallCar.addModel("suv", "SUV with sliding doors."); + smallCar.addModel("roadster", "Slick red roadster."); + + repository.save(smallCar); + assertThat(smallCar.getId()).isNotNull(); + assertThat(repository.findById(smallCar.getId()).get().getModels()).hasSize(2); + + Output.list(repository.findAll(), "Original LegoSet"); + + smallCar.getManual().setText("Just make it so it looks like a car."); + smallCar.addModel("pickup", "A pickup truck with some tools in the back."); + + repository.save(smallCar); + Output.list(repository.findAll(), "Updated"); + + smallCar.setManual(new Manual("One last attempt: Just build a car! Ok?", "Jens Schauder")); + + repository.save(smallCar); + Output.list(repository.findAll(), "Manual replaced"); + } + + private LegoSet createLegoSet() { + + LegoSet smallCar = new LegoSet(); + smallCar.setName("Small Car 01"); + return smallCar; + } +} diff --git a/jdbc/pom.xml b/jdbc/pom.xml index fc59c06f..0b50b4b9 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -18,6 +18,7 @@ basics + mybatis