diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 3f879915b7db3..100a8f761eb7b 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -32,6 +32,7 @@ 1.3.3 1.0.1 1.4.1 + 3.5.5 1.1.0 1.8.4 2.2.3 @@ -1221,6 +1222,16 @@ quarkus-mongodb-client-deployment ${project.version} + + io.quarkus + quarkus-mybatis + ${project.version} + + + io.quarkus + quarkus-mybatis-deployment + ${project.version} + io.quarkus quarkus-grpc @@ -2877,6 +2888,11 @@ mongodb-crypt ${mongo-crypt.version} + + org.mybatis + mybatis + ${mybatis.version} + net.minidev json-smart diff --git a/docs/src/main/asciidoc/mybatis.adoc b/docs/src/main/asciidoc/mybatis.adoc new file mode 100644 index 0000000000000..f8e343022706a --- /dev/null +++ b/docs/src/main/asciidoc/mybatis.adoc @@ -0,0 +1,274 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Using MyBatis +include::./attributes.adoc[] +:extension-status: preview + +This guide demonstrates how your Quarkus application can use link:https://mybatis.org/mybatis-3/[MyBatis] to support custom SQL, stored procedures and advanced mappings. + +include::./status-include.adoc[] + +== Prerequisites + +To complete this guide, you need: + +* less than 15 minutes +* an IDE +* JDK 1.8+ installed with `JAVA_HOME` configured appropriately +* Apache Maven {maven-version} +* A running Mysql Database server +* GraalVM, or Docker, installed if you want to run in native mode. + +== Architecture + +The application built in this guide is quite simple: the user can get, add and remove a record through the RESTful API by using the MyBatis Mapper. + + +== Solution + +We recommend that you follow the instructions in the next sections and create the application step by step. + +== Creating the Maven Project + +First, we need a new project. Create a new project with the following command: + +[source, subs=attributes+] +---- +mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=mybatis-quickstart \ + -Dextensions="mybatis,restesay-jackson,jdbc-mysql" +cd mybatis-quickstart +---- +This command generates a Maven project, with its pom.xml importing the quarkus-mybatis extension. + +If you already have your Quarkus project configured, you can add the `mybatis` extension +to your project by running the following command in your project base directory: + +[source,bash] +---- +./mvnw quarkus:add-extension -Dextensions="mybatis" +---- + +This will add the following to your `pom.xml`: + +[source] +---- + + io.quarkus + quarkus-mybatis + +---- + +== Creating the User POJO +We are going to create a `User` POJO to access to the data in the backend mysql server. +Create the `src/main/java/org/acme/mybatis/User.java` file, with the following content: + +[source, java] +---- +package org.acme.mybatis; + +public class User { + private Integer id; + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +---- + +== Creating the User Mapper +We are going to create a `UserMapper` class which will use the MyBatis annotations to inject the SQL. +Create the `src/main/java/org/acme/mybatis/UserMapper.java` file, with the following content: + +[source, java] +---- +package org.acme.mybatis; + +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface UserMapper { + + @Select("SELECT * FROM USERS WHERE id = #{id}") + User getUser(Integer id); // <1> + + @Insert("INSERT INTO USERS (id, name) VALUES (#{id}, #{name})") + Integer createUser(@Param("id") Integer id, @Param("name") String name); // <2> + + @Delete("DELETE FROM USERS WHERE id = #{id}") + Integer removeUser(Integer id); // <3> +} +---- + +1. Get a user from the database. +2. Insert a user into the database. We should use the `@Param` to bind the parameters. +3. Delete a user from the databse. + +== Creating the MyBatisResource to handle the requests +We are going to create a `MyBatisResource` class which will handle all the requests to create, query or remove the data +from the database. + +[source, java] +---- +package org.acme.mybatis; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/mybatis") +public class MyBatisResource { + + @Inject + UserMapper userMapper; // <1> + + @Path("/user/{id}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public User getUser(@PathParam("id") Integer id) { + return userMapper.getUser(id); + } + + @Path("/user") + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Integer createUser(@FormParam("id") Integer id, @FormParam("name") String name) { + return userMapper.createUser(id, name); + } + + @Path("/user/{id}") + @DELETE + @Produces(MediaType.TEXT_PLAIN) + public Integer removeUser(@PathParam("id") Integer id) { + return userMapper.removeUser(id); + } +} +---- +1. It uses the UserMapper which should be injected by the Quarkus to access the database. + +== Configure the properties + +We need to config the datasource used to connect to the database and the mybatis will choose the default one. Also you +can use ```quarkus.mybatis.datasource``` for the specific database. + +[source] +---- +quarkus.datasource.db-kind=mysql +quarkus.datasource.username= + +quarkus.datasource.jdbc.url=jdbc:mysql://localhost/test #<1> +quarkus.mybatis.initial-sql=insert.sql #<2> +---- + +1. The datasource used by the mybatis to connect the database. +2. The SQL file which should be executed just after the application is started. + +We could keep the following content in the `insert.sql` to add some data: +[source, sql] +---- +DROP TABLE IF EXISTS USERS; + +CREATE TABLE USERS ( + id integer not null primary key, + name varchar(80) not null +); + +INSERT INTO USERS (id, name) values(1, 'Test User1'); +INSERT INTO USERS (id, name) values(2, 'Test User2'); +INSERT INTO USERS (id, name) values(3, 'Test User3'); +---- + +== Running with the JVM mode +At first, you should make sure the Mysql Server is running and the `test` database has been created. +Then, you just need to run: + +[source, shell] +---- +./mvnw compile quarkus:dev +---- + +You can get the user by using the following command: + +[source, shell] +---- +curl http://localhost:8080/mybatis/user/1 +---- + +Or create a new user: + +[source, shell] +---- +curl -X POST http://localhost:8080/mybatis/user -d 'id=4&name=test' +---- + +Or remove a user: + +[source, shell] +---- +curl -X DELETE http://localhost:8080/mybatis/user/1 +---- + +== Running Native +You have to add the `--report-unsupported-elements-at-runtime` option when buiding the native image now. +So add the following content with the native profile in the `pom.xml`: + +[source, xml] +---- + + native + + + --report-unsupported-elements-at-runtime + + + +---- + +You can build the native executable with: + +[source, shell] +---- +./mvnw package -Pnative +---- + +and then run with: + +[source, shell] +---- +./target/mybatis-quickstart-1.0-SNAPSHOT-runner +---- + +== Using @CacheNamespace +You have to set `readWrite=false` when building the native image since the `ObjectOutputStream.writeObject` has not been supported by Graal VM. + +== Configuration References +include::{generated-dir}/config/quarkus-mybatis.adoc[opts=optional, leveloffset=+1] diff --git a/extensions/mybatis/deployment/pom.xml b/extensions/mybatis/deployment/pom.xml new file mode 100644 index 0000000000000..0ac31c599cde6 --- /dev/null +++ b/extensions/mybatis/deployment/pom.xml @@ -0,0 +1,64 @@ + + + + quarkus-mybatis-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-mybatis-deployment + Quarkus - Mybatis - Deployment + + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-agroal-deployment + + + io.quarkus + quarkus-mybatis + + + + io.quarkus + quarkus-junit5-internal + test + + + io.quarkus + quarkus-test-h2 + test + + + io.quarkus + quarkus-jdbc-h2-deployment + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/MyBatisMapperBuildItem.java b/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/MyBatisMapperBuildItem.java new file mode 100644 index 0000000000000..925d79c243aef --- /dev/null +++ b/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/MyBatisMapperBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.mybatis.deployment; + +import org.jboss.jandex.DotName; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class MyBatisMapperBuildItem extends MultiBuildItem { + private final DotName mapperName; + + public MyBatisMapperBuildItem(DotName mapperName) { + this.mapperName = mapperName; + } + + public DotName getMapperName() { + return mapperName; + } +} diff --git a/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/MybatisProcessor.java b/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/MybatisProcessor.java new file mode 100644 index 0000000000000..db6d242ebe50d --- /dev/null +++ b/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/MybatisProcessor.java @@ -0,0 +1,175 @@ +package io.quarkus.mybatis.deployment; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.inject.Singleton; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.cache.decorators.LruCache; +import org.apache.ibatis.cache.impl.PerpetualCache; +import org.apache.ibatis.javassist.util.proxy.ProxyFactory; +import org.apache.ibatis.logging.log4j.Log4jImpl; +import org.apache.ibatis.scripting.defaults.RawLanguageDriver; +import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.DotName; +import org.jboss.logging.Logger; + +import io.quarkus.agroal.deployment.JdbcDataSourceBuildItem; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.deployment.configuration.ConfigurationError; +import io.quarkus.mybatis.runtime.MyBatisProducers; +import io.quarkus.mybatis.runtime.MyBatisRecorder; +import io.quarkus.mybatis.runtime.MyBatisRuntimeConfig; + +class MybatisProcessor { + + private static final Logger LOG = Logger.getLogger(MybatisProcessor.class); + private static final String FEATURE = "mybatis"; + private static final DotName MYBATIS_MAPPER = DotName.createSimple(Mapper.class.getName()); + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + void runtimeInitialzed(BuildProducer runtimeInit) { + runtimeInit.produce(new RuntimeInitializedClassBuildItem(Log4jImpl.class.getName())); + } + + @BuildStep + void reflectiveClasses(BuildProducer reflectiveClass) { + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, + ProxyFactory.class, + XMLLanguageDriver.class, + RawLanguageDriver.class)); + + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, + PerpetualCache.class, LruCache.class)); + } + + @BuildStep + void addMyBatisMappers(BuildProducer mappers, + BuildProducer reflective, + BuildProducer proxy, + CombinedIndexBuildItem indexBuildItem) { + for (AnnotationInstance i : indexBuildItem.getIndex().getAnnotations(MYBATIS_MAPPER)) { + if (i.target().kind() == AnnotationTarget.Kind.CLASS) { + DotName dotName = i.target().asClass().name(); + mappers.produce(new MyBatisMapperBuildItem(dotName)); + reflective.produce(new ReflectiveClassBuildItem(true, false, dotName.toString())); + proxy.produce(new NativeImageProxyDefinitionBuildItem(dotName.toString())); + } + } + } + + @BuildStep + void unremovableBeans(BuildProducer beanProducer) { + beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(MyBatisProducers.class)); + } + + @BuildStep + void initalSql(BuildProducer resource, MyBatisRuntimeConfig config) { + if (config.initialSql.isPresent()) { + resource.produce(new NativeImageResourceBuildItem(config.initialSql.get())); + } + } + + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + SqlSessionFactoryBuildItem generateSqlSessionFactory(MyBatisRuntimeConfig myBatisRuntimeConfig, + List myBatisMapperBuildItems, + List jdbcDataSourcesBuildItem, + MyBatisRecorder recorder) { + List mappers = myBatisMapperBuildItems + .stream().map(m -> m.getMapperName().toString()).collect(Collectors.toList()); + + String dataSourceName; + if (myBatisRuntimeConfig.dataSource.isPresent()) { + dataSourceName = myBatisRuntimeConfig.dataSource.get(); + Optional jdbcDataSourceBuildItem = jdbcDataSourcesBuildItem.stream() + .filter(i -> i.getName().equals(dataSourceName)) + .findFirst(); + if (!jdbcDataSourceBuildItem.isPresent()) { + throw new ConfigurationError("Can not find datasource " + dataSourceName); + } + } else { + Optional defaultJdbcDataSourceBuildItem = jdbcDataSourcesBuildItem.stream() + .filter(i -> i.isDefault()) + .findFirst(); + if (defaultJdbcDataSourceBuildItem.isPresent()) { + dataSourceName = defaultJdbcDataSourceBuildItem.get().getName(); + } else { + throw new ConfigurationError("No default datasource"); + } + } + + return new SqlSessionFactoryBuildItem(recorder.createSqlSessionFactory( + myBatisRuntimeConfig.environment, + myBatisRuntimeConfig.transactionFactory, + dataSourceName, + mappers)); + } + + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + SqlSessionManagerBuildItem generateSqlSessionManager(SqlSessionFactoryBuildItem sqlSessionFactoryBuildItem, + MyBatisRecorder recorder) { + return new SqlSessionManagerBuildItem(recorder.createSqlSessionManager( + sqlSessionFactoryBuildItem.getSqlSessionFactory())); + } + + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + void generateMapperBeans(MyBatisRecorder recorder, + List myBatisMapperBuildItems, + SqlSessionManagerBuildItem sqlSessionManagerBuildItem, + BuildProducer syntheticBeanBuildItemBuildProducer) { + + for (MyBatisMapperBuildItem i : myBatisMapperBuildItems) { + SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem + .configure(i.getMapperName()) + .scope(Singleton.class) + .setRuntimeInit() + .unremovable() + .supplier(recorder.MyBatisMapperSupplier(i.getMapperName().toString(), + sqlSessionManagerBuildItem.getSqlSessionManager())); + syntheticBeanBuildItemBuildProducer.produce(configurator.done()); + } + } + + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + void register(SqlSessionFactoryBuildItem sqlSessionFactoryBuildItem, + BeanContainerBuildItem beanContainerBuildItem, + MyBatisRecorder recorder) { + recorder.register(sqlSessionFactoryBuildItem.getSqlSessionFactory(), beanContainerBuildItem.getValue()); + } + + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + void runInitialSql(SqlSessionFactoryBuildItem sqlSessionFactoryBuildItem, + MyBatisRuntimeConfig myBatisRuntimeConfig, + MyBatisRecorder recorder) { + if (myBatisRuntimeConfig.initialSql.isPresent()) { + recorder.runInitialSql(sqlSessionFactoryBuildItem.getSqlSessionFactory(), + myBatisRuntimeConfig.initialSql.get()); + } + } +} diff --git a/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/SqlSessionFactoryBuildItem.java b/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/SqlSessionFactoryBuildItem.java new file mode 100644 index 0000000000000..38decde6b4085 --- /dev/null +++ b/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/SqlSessionFactoryBuildItem.java @@ -0,0 +1,21 @@ +package io.quarkus.mybatis.deployment; + +import org.apache.ibatis.session.SqlSessionFactory; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.runtime.RuntimeValue; + +/** + * Hold the RuntimeValue of {@link SqlSessionFactory} + */ +public final class SqlSessionFactoryBuildItem extends SimpleBuildItem { + private final RuntimeValue sqlSessionFactory; + + public SqlSessionFactoryBuildItem(RuntimeValue sqlSessionFactory) { + this.sqlSessionFactory = sqlSessionFactory; + } + + public RuntimeValue getSqlSessionFactory() { + return sqlSessionFactory; + } +} diff --git a/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/SqlSessionManagerBuildItem.java b/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/SqlSessionManagerBuildItem.java new file mode 100644 index 0000000000000..2b19d7d674ad2 --- /dev/null +++ b/extensions/mybatis/deployment/src/main/java/io/quarkus/mybatis/deployment/SqlSessionManagerBuildItem.java @@ -0,0 +1,21 @@ +package io.quarkus.mybatis.deployment; + +import org.apache.ibatis.session.SqlSessionManager; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.runtime.RuntimeValue; + +/** + * Hold the RuntimeValue of {@link SqlSessionManager} + */ +public final class SqlSessionManagerBuildItem extends SimpleBuildItem { + private final RuntimeValue sqlSessionManager; + + public SqlSessionManagerBuildItem(RuntimeValue sqlSessionManager) { + this.sqlSessionManager = sqlSessionManager; + } + + public RuntimeValue getSqlSessionManager() { + return sqlSessionManager; + } +} diff --git a/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/MyBatisTest.java b/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/MyBatisTest.java new file mode 100644 index 0000000000000..77d9f1724949b --- /dev/null +++ b/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/MyBatisTest.java @@ -0,0 +1,34 @@ +package io.quarkus.mybatis.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.inject.Inject; + +import org.apache.ibatis.session.SqlSessionFactory; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class MyBatisTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().withConfigurationResource("application.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(UserMapper.class, User.class)); + + @Inject + UserMapper userMapper; + + @Inject + SqlSessionFactory sqlSessionFactory; + + @Test + public void test() throws Exception { + assertTrue(sqlSessionFactory.getConfiguration().getMapperRegistry().hasMapper(UserMapper.class)); + User user = userMapper.getUser(1); + assertEquals(user.getId(), 1); + assertEquals(user.getName(), "Test User1"); + } +} diff --git a/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/TestResources.java b/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/TestResources.java new file mode 100644 index 0000000000000..463ef736ab47a --- /dev/null +++ b/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/TestResources.java @@ -0,0 +1,8 @@ +package io.quarkus.mybatis.test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.h2.H2DatabaseTestResource; + +@QuarkusTestResource(H2DatabaseTestResource.class) +public class TestResources { +} diff --git a/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/User.java b/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/User.java new file mode 100644 index 0000000000000..22a2703b93b43 --- /dev/null +++ b/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/User.java @@ -0,0 +1,22 @@ +package io.quarkus.mybatis.test; + +public class User { + private Integer id; + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/UserMapper.java b/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/UserMapper.java new file mode 100644 index 0000000000000..f0c2bc6fceeb6 --- /dev/null +++ b/extensions/mybatis/deployment/src/test/java/io/quarkus/mybatis/test/UserMapper.java @@ -0,0 +1,11 @@ +package io.quarkus.mybatis.test; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface UserMapper { + + @Select("select * from users where id = #{id}") + User getUser(Integer id); +} diff --git a/extensions/mybatis/deployment/src/test/resources/application.properties b/extensions/mybatis/deployment/src/test/resources/application.properties new file mode 100644 index 0000000000000..cfdfa74d72918 --- /dev/null +++ b/extensions/mybatis/deployment/src/test/resources/application.properties @@ -0,0 +1,5 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=username-default + +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:default +quarkus.mybatis.initial-sql=insert.sql diff --git a/extensions/mybatis/deployment/src/test/resources/insert.sql b/extensions/mybatis/deployment/src/test/resources/insert.sql new file mode 100644 index 0000000000000..a6edad4e71398 --- /dev/null +++ b/extensions/mybatis/deployment/src/test/resources/insert.sql @@ -0,0 +1,9 @@ +CREATE TABLE USERS ( + id integer not null primary key, + name varchar(80) not null +); + +DELETE FROM users; +insert into users (id, name) values(1, 'Test User1'); +insert into users (id, name) values(2, 'Test User2'); +insert into users (id, name) values(3, 'Test User3'); diff --git a/extensions/mybatis/pom.xml b/extensions/mybatis/pom.xml new file mode 100644 index 0000000000000..0d46a93bc5256 --- /dev/null +++ b/extensions/mybatis/pom.xml @@ -0,0 +1,21 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + 4.0.0 + + quarkus-mybatis-parent + Quarkus - Mybatis + pom + + + deployment + runtime + + diff --git a/extensions/mybatis/runtime/pom.xml b/extensions/mybatis/runtime/pom.xml new file mode 100644 index 0000000000000..ee55066abf1e3 --- /dev/null +++ b/extensions/mybatis/runtime/pom.xml @@ -0,0 +1,56 @@ + + + + quarkus-mybatis-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-mybatis + Quarkus - Mybatis - Runtime + MyBatis SQL mapper framework for Java + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-agroal + + + org.mybatis + mybatis + + + org.graalvm.nativeimage + svm + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/MyBatisProducers.java b/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/MyBatisProducers.java new file mode 100644 index 0000000000000..a3c95ae529ca9 --- /dev/null +++ b/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/MyBatisProducers.java @@ -0,0 +1,21 @@ +package io.quarkus.mybatis.runtime; + +import javax.enterprise.inject.Produces; +import javax.inject.Singleton; + +import org.apache.ibatis.session.SqlSessionFactory; + +@Singleton +public class MyBatisProducers { + private volatile SqlSessionFactory sqlSessionFactory; + + public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { + this.sqlSessionFactory = sqlSessionFactory; + } + + @Singleton + @Produces + SqlSessionFactory sqlSessionFactory() { + return this.sqlSessionFactory; + } +} diff --git a/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/MyBatisRecorder.java b/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/MyBatisRecorder.java new file mode 100644 index 0000000000000..8080ab38c5a3f --- /dev/null +++ b/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/MyBatisRecorder.java @@ -0,0 +1,88 @@ +package io.quarkus.mybatis.runtime; + +import java.io.Reader; +import java.sql.Connection; +import java.util.List; +import java.util.function.Supplier; + +import org.apache.ibatis.io.Resources; +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.session.SqlSessionManager; +import org.apache.ibatis.transaction.TransactionFactory; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.apache.ibatis.transaction.managed.ManagedTransactionFactory; +import org.jboss.logging.Logger; + +import io.quarkus.agroal.runtime.DataSources; +import io.quarkus.arc.runtime.BeanContainer; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class MyBatisRecorder { + private static final Logger LOG = Logger.getLogger(MyBatisRecorder.class); + + public RuntimeValue createSqlSessionFactory( + String environment, String transactionFactory, String dataSourceName, List mappers) { + Configuration configuration = new Configuration(); + + TransactionFactory factory; + if (transactionFactory.equals("MANAGED")) { + factory = new ManagedTransactionFactory(); + } else { + factory = new JdbcTransactionFactory(); + } + + Environment.Builder environmentBuilder = new Environment.Builder(environment).transactionFactory(factory).dataSource( + DataSources.fromName(dataSourceName)); + + configuration.setEnvironment(environmentBuilder.build()); + for (String mapper : mappers) { + try { + configuration.addMapper(Resources.classForName(mapper)); + } catch (ClassNotFoundException e) { + LOG.debug("Can not find the mapper class " + mapper); + } + } + + SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); + return new RuntimeValue<>(sqlSessionFactory); + } + + public RuntimeValue createSqlSessionManager(RuntimeValue sqlSessionFactory) { + SqlSessionManager sqlSessionManager = SqlSessionManager.newInstance(sqlSessionFactory.getValue()); + return new RuntimeValue<>(sqlSessionManager); + } + + public Supplier MyBatisMapperSupplier(String name, RuntimeValue sqlSessionManager) { + return () -> { + try { + return sqlSessionManager.getValue().getMapper(Resources.classForName(name)); + } catch (ClassNotFoundException e) { + return null; + } + }; + } + + public void runInitialSql(RuntimeValue sqlSessionFactory, String sql) { + try (SqlSession session = sqlSessionFactory.getValue().openSession()) { + Connection conn = session.getConnection(); + Reader reader = Resources.getResourceAsReader(sql); + ScriptRunner runner = new ScriptRunner(conn); + runner.setLogWriter(null); + runner.runScript(reader); + reader.close(); + } catch (Exception e) { + LOG.warn("Error executing SQL Script " + sql); + } + } + + public void register(RuntimeValue sqlSessionFactory, BeanContainer beanContainer) { + beanContainer.instance(MyBatisProducers.class).setSqlSessionFactory(sqlSessionFactory.getValue()); + } +} diff --git a/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/MyBatisRuntimeConfig.java b/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/MyBatisRuntimeConfig.java new file mode 100644 index 0000000000000..0bf8c5a7df05c --- /dev/null +++ b/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/MyBatisRuntimeConfig.java @@ -0,0 +1,34 @@ +package io.quarkus.mybatis.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "mybatis") +public class MyBatisRuntimeConfig { + + /** + * MyBatis environment id + */ + @ConfigItem(defaultValue = "quarkus") + public String environment; + + /** + * MyBatis transaction factory + */ + @ConfigItem(defaultValue = "MANAGED") + public String transactionFactory; + + /** + * MyBatis data source + */ + @ConfigItem(name = "datasource") + public Optional dataSource; + + /** + * MyBatis initial sql + */ + @ConfigItem(name = "initial-sql") + public Optional initialSql; +} diff --git a/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/graal/DefineClassHelperSubstitution.java b/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/graal/DefineClassHelperSubstitution.java new file mode 100644 index 0000000000000..382444d04850d --- /dev/null +++ b/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/graal/DefineClassHelperSubstitution.java @@ -0,0 +1,25 @@ +package io.quarkus.mybatis.runtime.graal; + +import java.lang.invoke.MethodHandles; + +import org.apache.ibatis.javassist.CannotCompileException; +import org.apache.ibatis.javassist.util.proxy.DefineClassHelper; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.jdk.JDK8OrEarlier; + +@TargetClass(value = DefineClassHelper.class, onlyWith = JDK8OrEarlier.class) +final public class DefineClassHelperSubstitution { + + @Substitute + public static Class toClass(MethodHandles.Lookup lookup, byte[] bcode) throws CannotCompileException { + throw new CannotCompileException("Not support"); + } + + @Substitute + static Class toPublicClass(String className, byte[] bcode) throws CannotCompileException { + throw new CannotCompileException("Not support"); + } + +} diff --git a/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/graal/SerializedCacheSubstitution.java b/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/graal/SerializedCacheSubstitution.java new file mode 100644 index 0000000000000..a04684e6d3caa --- /dev/null +++ b/extensions/mybatis/runtime/src/main/java/io/quarkus/mybatis/runtime/graal/SerializedCacheSubstitution.java @@ -0,0 +1,18 @@ +package io.quarkus.mybatis.runtime.graal; + +import java.io.Serializable; + +import org.apache.ibatis.cache.CacheException; +import org.apache.ibatis.cache.decorators.SerializedCache; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(SerializedCache.class) +final public class SerializedCacheSubstitution { + + @Substitute + private byte[] serialize(Serializable value) { + throw new CacheException("ObjectOutputStream.writeObject is unsupported in Graal VM"); + } +} diff --git a/extensions/mybatis/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/mybatis/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..2ecf549c519d8 --- /dev/null +++ b/extensions/mybatis/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +--- +name: "MyBatis SQL Mapper" +metadata: + keywords: + - "mybatis" + guide: "https://quarkus.io/guides/mybatis" + categories: + - "data" + status: "preview" diff --git a/extensions/pom.xml b/extensions/pom.xml index ed53e95a036dc..a51ebcaec2b97 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -100,6 +100,7 @@ tika neo4j mongodb-client + mybatis artemis-core artemis-jms avro diff --git a/integration-tests/mybatis/pom.xml b/integration-tests/mybatis/pom.xml new file mode 100644 index 0000000000000..263958e91ee38 --- /dev/null +++ b/integration-tests/mybatis/pom.xml @@ -0,0 +1,121 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-mybatis-integration-test + Quarkus - Integration Tests - MyBatis + The mybatis integration tests module + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-resteasy-jackson + + + io.quarkus + quarkus-mybatis + + + io.quarkus + quarkus-jdbc-h2 + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-test-h2 + test + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + native-image + + + native + + + + --report-unsupported-elements-at-runtime + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + io.quarkus + quarkus-maven-plugin + + + native-image + + native-image + + + true + true + ${graalvmHome} + + + + + + + + + + diff --git a/integration-tests/mybatis/src/main/java/io/quarkus/it/mybatis/MybatisResource.java b/integration-tests/mybatis/src/main/java/io/quarkus/it/mybatis/MybatisResource.java new file mode 100644 index 0000000000000..e92076fca3497 --- /dev/null +++ b/integration-tests/mybatis/src/main/java/io/quarkus/it/mybatis/MybatisResource.java @@ -0,0 +1,42 @@ +package io.quarkus.it.mybatis; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/mybatis") +public class MybatisResource { + + @Inject + UserMapper userMapper; + + @Path("/user/{id}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public User getUser(@PathParam("id") Integer id) { + return userMapper.getUser(id); + } + + @Path("/user") + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Integer createUser(@FormParam("id") Integer id, @FormParam("name") String name) { + return userMapper.createUser(id, name); + } + + @Path("/user/{id}") + @DELETE + @Produces(MediaType.TEXT_PLAIN) + public Integer removeUser(@PathParam("id") Integer id) { + return userMapper.removeUser(id); + } + +} diff --git a/integration-tests/mybatis/src/main/java/io/quarkus/it/mybatis/User.java b/integration-tests/mybatis/src/main/java/io/quarkus/it/mybatis/User.java new file mode 100644 index 0000000000000..9e6c0b0e5c36e --- /dev/null +++ b/integration-tests/mybatis/src/main/java/io/quarkus/it/mybatis/User.java @@ -0,0 +1,24 @@ +package io.quarkus.it.mybatis; + +import java.io.Serializable; + +public class User implements Serializable { + private Integer id; + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/integration-tests/mybatis/src/main/java/io/quarkus/it/mybatis/UserMapper.java b/integration-tests/mybatis/src/main/java/io/quarkus/it/mybatis/UserMapper.java new file mode 100644 index 0000000000000..fc7e7a9280871 --- /dev/null +++ b/integration-tests/mybatis/src/main/java/io/quarkus/it/mybatis/UserMapper.java @@ -0,0 +1,22 @@ +package io.quarkus.it.mybatis; + +import org.apache.ibatis.annotations.CacheNamespace; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +@CacheNamespace(readWrite = false) +public interface UserMapper { + + @Select("select * from users where id = #{id}") + User getUser(Integer id); + + @Insert("insert into users (id, name) values (#{id}, #{name})") + Integer createUser(@Param("id") Integer id, @Param("name") String name); + + @Delete("delete from users where id = #{id}") + Integer removeUser(Integer id); +} diff --git a/integration-tests/mybatis/src/main/resources/application.properties b/integration-tests/mybatis/src/main/resources/application.properties new file mode 100644 index 0000000000000..cfdfa74d72918 --- /dev/null +++ b/integration-tests/mybatis/src/main/resources/application.properties @@ -0,0 +1,5 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=username-default + +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:default +quarkus.mybatis.initial-sql=insert.sql diff --git a/integration-tests/mybatis/src/main/resources/insert.sql b/integration-tests/mybatis/src/main/resources/insert.sql new file mode 100644 index 0000000000000..a6edad4e71398 --- /dev/null +++ b/integration-tests/mybatis/src/main/resources/insert.sql @@ -0,0 +1,9 @@ +CREATE TABLE USERS ( + id integer not null primary key, + name varchar(80) not null +); + +DELETE FROM users; +insert into users (id, name) values(1, 'Test User1'); +insert into users (id, name) values(2, 'Test User2'); +insert into users (id, name) values(3, 'Test User3'); diff --git a/integration-tests/mybatis/src/test/java/io/quarkus/it/mybatis/MybatisIT.java b/integration-tests/mybatis/src/test/java/io/quarkus/it/mybatis/MybatisIT.java new file mode 100644 index 0000000000000..f5a043f3a765b --- /dev/null +++ b/integration-tests/mybatis/src/test/java/io/quarkus/it/mybatis/MybatisIT.java @@ -0,0 +1,8 @@ +package io.quarkus.it.mybatis; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +class MybatisIT extends MybatisTest { + +} diff --git a/integration-tests/mybatis/src/test/java/io/quarkus/it/mybatis/MybatisTest.java b/integration-tests/mybatis/src/test/java/io/quarkus/it/mybatis/MybatisTest.java new file mode 100644 index 0000000000000..ebf9e9d23f817 --- /dev/null +++ b/integration-tests/mybatis/src/test/java/io/quarkus/it/mybatis/MybatisTest.java @@ -0,0 +1,25 @@ +package io.quarkus.it.mybatis; + +import static org.hamcrest.core.Is.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +class MybatisTest { + + @Test + public void test() { + RestAssured.when().get("/mybatis/user/1").then() + .body(is("{\"id\":1,\"name\":\"Test User1\"}")); + + RestAssured.given().param("id", "5").param("name", "New User").post("/mybatis/user") + .then().body(is("1")); + + RestAssured.when().delete("/mybatis/user/1").then() + .body(is("1")); + } + +} diff --git a/integration-tests/mybatis/src/test/java/io/quarkus/it/mybatis/TestResources.java b/integration-tests/mybatis/src/test/java/io/quarkus/it/mybatis/TestResources.java new file mode 100644 index 0000000000000..d50dcae756daa --- /dev/null +++ b/integration-tests/mybatis/src/test/java/io/quarkus/it/mybatis/TestResources.java @@ -0,0 +1,8 @@ +package io.quarkus.it.mybatis; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.h2.H2DatabaseTestResource; + +@QuarkusTestResource(H2DatabaseTestResource.class) +public class TestResources { +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index c6146c8e36e42..c20b36a78e1aa 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -88,6 +88,7 @@ tika neo4j mongodb-client + mybatis jackson jsonb resteasy-jackson