diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000000..e50c7ea439 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +lombok.nonNull.exceptionType = IllegalArgumentException +lombok.log.fieldName = LOG diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000..a0bb887d5d --- /dev/null +++ b/pom.xml @@ -0,0 +1,237 @@ + + + + 4.0.0 + + org.springframework.data + spring-data-jdbc + 1.0.0.DATAJDBC-99-SNAPSHOT + + Spring Data JDBC + Spring Data module for JDBC repositories. + http://projects.spring.io/spring-data-jdbc + + + org.springframework.data.build + spring-data-parent + 2.0.0.BUILD-SNAPSHOT + + + + + DATAJDBC + + 2.0.0.BUILD-SNAPSHOT + + reuseReports + 1.8.0.10 + + + + + + release + + + + org.jfrog.buildinfo + artifactory-maven-plugin + false + + + + + + + all-dbs + + + + org.apache.maven.plugins + maven-surefire-plugin + + + mysql-test + test + + test + + + + **/*IntegrationTests.java + + + mysql + + + + + postgres-test + test + + test + + + + **/*IntegrationTests.java + + + postgres + + + + + + + + + + + + + + ${project.groupId} + spring-data-commons + ${springdata.commons} + + + + org.springframework + spring-tx + + + + org.springframework + spring-context + + + + org.springframework + spring-beans + + + + org.springframework + spring-jdbc + + + + org.springframework + spring-core + + + commons-logging + commons-logging + + + + + + org.hsqldb + hsqldb + 2.2.8 + test + + + + org.assertj + assertj-core + 3.6.2 + test + + + + mysql + mysql-connector-java + 5.1.41 + test + + + + org.postgresql + postgresql + 42.0.0 + test + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12 + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco} + + ${jacoco.destfile} + + + + jacoco-initialize + + prepare-agent + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + + **/*Tests.java + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.codehaus.mojo + wagon-maven-plugin + + + org.asciidoctor + asciidoctor-maven-plugin + + + + + + + spring-libs-snapshot + https://repo.spring.io/libs-snapshot + + + + + + spring-plugins-snapshot + https://repo.spring.io/plugins-snapshot + + + + diff --git a/readme.md b/readme.md index 62f3e743f9..e2beccbb41 100644 --- a/readme.md +++ b/readme.md @@ -9,6 +9,35 @@ The primary goal of the [Spring Data](http://projects.spring.io/spring-data) pro ## Quick Start ## +## Execute Tests ## + +### Fast running tests + +Fast running tests can executed with a simple + + mvn test + +This will execute unit tests and integration tests using an in-memory database. + +### Running tests with a real database + +To run the integration tests against a specific database you nned to have the database running on your local machine and then execute. + + mvn test -Dspring.profiles.active= + +This will also execute the unit tests. + +Currently the following *databasetypes* are available: + +* hsql (default, does not need to be running) +* mysql + +### Run tests with all databases + + mvn test -Pall-dbs + +This will execute the unit tests, and all the integration tests with all the databases we currently support for testing. The databases must be running. + ## Contributing to Spring Data JDBC ## Here are some ways for you to get involved in the community: diff --git a/run-tests-against-all-dbs.sh b/run-tests-against-all-dbs.sh new file mode 100755 index 0000000000..9ef6389c1c --- /dev/null +++ b/run-tests-against-all-dbs.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +./start-all-dbs.sh && mvn clean install -Pall-dbs && ./stop-all-dbs.sh diff --git a/src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java b/src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java new file mode 100644 index 0000000000..9d228ce780 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.context; + +import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentProperty; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityImpl; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; +import org.springframework.data.mapping.context.AbstractMappingContext; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.TypeInformation; + +/** + * {@link MappingContext} implementation for JDBC. + * + * @author Jens Schauder + * @since 2.0 + */ +public class JdbcMappingContext extends AbstractMappingContext, JdbcPersistentProperty> { + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) + */ + @Override + protected JdbcPersistentEntityImpl createPersistentEntity(TypeInformation typeInformation) { + return new JdbcPersistentEntityImpl<>(typeInformation); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentProperty(org.springframework.data.mapping.model.Property, org.springframework.data.mapping.model.MutablePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder) + */ + @Override + protected JdbcPersistentProperty createPersistentProperty(Property property, JdbcPersistentEntityImpl owner, + SimpleTypeHolder simpleTypeHolder) { + return new BasicJdbcPersistentProperty(property, owner, simpleTypeHolder); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterCreation.java b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterCreation.java new file mode 100644 index 0000000000..b0f2e8edd6 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterCreation.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * Gets published after instantiation and setting of all the properties of an entity. This allows to do some + * postprocessing of entities. + * + * @author Jens Schauder + * @since 2.0 + */ +public class AfterCreation extends JdbcEventWithIdAndEntity { + + private static final long serialVersionUID = -4185777271143436728L; + + /** + * @param id of the entity + * @param entity the newly instantiated entity. + */ + public AfterCreation(Specified id, Object entity) { + super(id, entity); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterDelete.java b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterDelete.java new file mode 100644 index 0000000000..f1f24d1952 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterDelete.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import java.util.Optional; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * Gets published after deletion of an entity. It will have a {@link Specified} identifier. If the entity is empty or + * not depends on the delete method used. + * + * @author Jens Schauder + * @since 2.0 + */ +public class AfterDelete extends JdbcEventWithId { + + private static final long serialVersionUID = 3594807189931141582L; + + /** + * @param id of the entity. + * @param instance the deleted entity if it is available. + */ + public AfterDelete(Specified id, Optional instance) { + super(id, instance); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterInsert.java b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterInsert.java new file mode 100644 index 0000000000..5b82bd3c93 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterInsert.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * Gets published after an entity got inserted into the database. + * + * @author Jens Schauder + * @since 2.0 + */ +public class AfterInsert extends AfterSave { + + private static final long serialVersionUID = 1312645717283677063L; + + /** + * @param id identifier of the entity triggering the event. + * @param instance the newly inserted entity. + */ + public AfterInsert(Specified id, Object instance) { + super(id, instance); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterSave.java b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterSave.java new file mode 100644 index 0000000000..9c79e21398 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterSave.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * Subclasses of this get published after a new instance or a changed instance was saved in the database. + * + * @author Jens Schauder + * @since 2.0 + */ +public class AfterSave extends JdbcEventWithIdAndEntity { + + private static final long serialVersionUID = 8982085767296982848L; + + /** + * @param id identifier of + * @param instance the newly saved entity. + */ + AfterSave(Specified id, Object instance) { + super(id, instance); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterUpdate.java b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterUpdate.java new file mode 100644 index 0000000000..cda83304b2 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterUpdate.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * Gets published after an entity was updated in the database. + * + * @author Jens Schauder + * @since 2.0 + */ +public class AfterUpdate extends AfterSave { + + private static final long serialVersionUID = -1765706904721563399L; + + /** + * @param id of the entity + * @param instance the updated entity. + */ + public AfterUpdate(Specified id, Object instance) { + super(id, instance); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeDelete.java b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeDelete.java new file mode 100644 index 0000000000..ef7d1c0f12 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeDelete.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import java.util.Optional; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * Gets published when an entity is about to get deleted. + * + * @author Jens Schauder + * @since 2.0 + */ +public class BeforeDelete extends JdbcEventWithId { + + private static final long serialVersionUID = -5483432053368496651L; + + /** + * @param id the id of the entity + * @param entity the entity about to get deleted. Might be empty. + */ + public BeforeDelete(Specified id, Optional entity) { + super(id, entity); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeInsert.java b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeInsert.java new file mode 100644 index 0000000000..37d2227034 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeInsert.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +/** + * Gets published before an entity gets inserted into the database. When the id-property of the entity must get set + * manually, an event listener for this event may do so.
+ * The {@link Identifier} is {@link Unset#UNSET} + * + * @author Jens Schauder + * @since 2.0 + */ +public class BeforeInsert extends BeforeSave { + + private static final long serialVersionUID = -5350552051337308870L; + + /** + * @param instance the entity about to get inserted. + */ + public BeforeInsert(Object instance) { + super(Unset.UNSET, instance); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeSave.java b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeSave.java new file mode 100644 index 0000000000..ae100d39a4 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeSave.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +/** + * Subclasses of this get published before an entity gets saved to the database. + * + * @author Jens Schauder + * @since 2.0 + */ +public class BeforeSave extends JdbcEventWithEntity { + + private static final long serialVersionUID = -6996874391990315443L; + + /** + * @param id of the entity to be saved. + * @param instance the entity about to get saved. + */ + BeforeSave(Identifier id, Object instance) { + super(id, instance); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeUpdate.java b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeUpdate.java new file mode 100644 index 0000000000..57f7a330e1 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeUpdate.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * Gets published before an entity gets updated in the database. + * + * @author Jens Schauder + * @since 2.0 + */ +public class BeforeUpdate extends BeforeSave implements WithId { + + private static final long serialVersionUID = 794561215071613972L; + + private final Specified id; + + /** + * @param id of the entity about to get updated + * @param instance the entity about to get updated. + */ + public BeforeUpdate(Specified id, Object instance) { + + super(id, instance); + + this.id = id; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.mapping.event.JdbcEvent#getId() + */ + @Override + public Specified getId() { + return id; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/Identifier.java b/src/main/java/org/springframework/data/jdbc/mapping/event/Identifier.java new file mode 100644 index 0000000000..0db4664628 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/Identifier.java @@ -0,0 +1,79 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import java.util.Optional; + +import org.springframework.util.Assert; + +/** + * Wrapper for an identifier of an entity. Might either be a {@link Specified} or {@link Unset#UNSET} + * + * @author Jens Schauder + * @since 2.0 + */ +public interface Identifier { + + /** + * Creates a new {@link Specified} identifier for the given, non-null value. + * + * @param identifier must not be {@literal null}. + * @return will never be {@literal null}. + */ + static Specified of(Object identifier) { + + Assert.notNull(identifier, "Identifier must not be null!"); + + return SpecifiedIdentifier.of(identifier); + } + + /** + * Creates a new {@link Identifier} for the given optional source value. + * + * @param identifier must not be {@literal null}. + * @return + */ + static Identifier of(Optional identifier) { + + Assert.notNull(identifier, "Identifier must not be null!"); + + return identifier.map(it -> (Identifier) Identifier.of(it)).orElse(Unset.UNSET); + } + + /** + * Returns the identifier value. + * + * @return will never be {@literal null}. + */ + Optional getOptionalValue(); + + /** + * A specified identifier that exposes a definitely present identifier value. + * + * @author Oliver Gierke + */ + interface Specified extends Identifier { + + /** + * Returns the identifier value. + * + * @return will never be {@literal null}. + */ + default Object getValue() { + return getOptionalValue().orElseThrow(() -> new IllegalStateException("Should not happen!")); + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEvent.java b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEvent.java new file mode 100644 index 0000000000..d862517a34 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import java.util.Optional; + +/** + * + * @author Oliver Gierke + */ +public interface JdbcEvent { + + /** + * The identifier of the entity, triggering this event. Also available via {@link #getSource()}. + * + * @return the source of the event as an {@link Identifier}. + */ + Identifier getId(); + + /** + * Returns the entity the event was triggered for. + * + * @return will never be {@literal null}. + */ + Optional getOptionalEntity(); + +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithEntity.java b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithEntity.java new file mode 100644 index 0000000000..1d325f6bd5 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithEntity.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import java.util.Optional; + +/** + * A {@link SimpleJdbcEvent} which is guaranteed to have an entity. + * + * @author Jens Schauder + * @since 2.0 + */ +public class JdbcEventWithEntity extends SimpleJdbcEvent implements WithEntity { + + private static final long serialVersionUID = 4891455396602090638L; + + public JdbcEventWithEntity(Identifier id, Object entity) { + super(id, Optional.of(entity)); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithId.java b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithId.java new file mode 100644 index 0000000000..d5c1ded08a --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithId.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import java.util.Optional; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * A {@link SimpleJdbcEvent} guaranteed to have an identifier. + * + * @author Jens Schauder + * @since 2.0 + */ +public class JdbcEventWithId extends SimpleJdbcEvent implements WithId { + + private static final long serialVersionUID = -8071323168471611098L; + + private final Specified id; + + public JdbcEventWithId(Specified id, Optional entity) { + + super(id, entity); + + this.id = id; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.mapping.event.JdbcEvent#getId() + */ + @Override + public Specified getId() { + return id; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithIdAndEntity.java b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithIdAndEntity.java new file mode 100644 index 0000000000..e9268bf4b6 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithIdAndEntity.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import lombok.Getter; + +import java.util.Optional; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * A {@link SimpleJdbcEvent} which is guaranteed to have an identifier and an entity. + * + * @author Jens Schauder + * @since 2.0 + */ +@Getter +public class JdbcEventWithIdAndEntity extends JdbcEventWithId implements WithEntity { + + private static final long serialVersionUID = -3194462549552515519L; + + public JdbcEventWithIdAndEntity(Specified id, Object entity) { + super(id, Optional.of(entity)); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/SimpleJdbcEvent.java b/src/main/java/org/springframework/data/jdbc/mapping/event/SimpleJdbcEvent.java new file mode 100644 index 0000000000..bf5df81afa --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/SimpleJdbcEvent.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import java.util.Optional; + +import org.springframework.context.ApplicationEvent; + +/** + * The common superclass for all events published by JDBC repositories. {@link #getSource} contains the + * {@link Identifier} of the entity triggering the event. + * + * @author Jens Schauder + * @author Oliver Gierke + * @since 2.0 + */ +class SimpleJdbcEvent extends ApplicationEvent implements JdbcEvent { + + private static final long serialVersionUID = -1798807778668751659L; + + private final Object entity; + + SimpleJdbcEvent(Identifier id, Optional entity) { + + super(id); + + this.entity = entity.orElse(null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.mapping.event.JdbcEvent#getId() + */ + @Override + public Identifier getId() { + return (Identifier) getSource(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.mapping.event.JdbcEvent#getOptionalEntity() + */ + @Override + public Optional getOptionalEntity() { + return Optional.ofNullable(entity); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/SpecifiedIdentifier.java b/src/main/java/org/springframework/data/jdbc/mapping/event/SpecifiedIdentifier.java new file mode 100644 index 0000000000..a68c97b24e --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/SpecifiedIdentifier.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import lombok.Value; + +import java.util.Optional; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * Simple value object for {@link Specified}. + * + * @author Jens Schauder + * @author Oliver Gierke + * @since 2.0 + */ +@Value(staticConstructor = "of") +class SpecifiedIdentifier implements Specified { + + Object value; + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.mapping.event.Identifier#getOptionalValue() + */ + @Override + public Optional getOptionalValue() { + return Optional.of(value); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/Unset.java b/src/main/java/org/springframework/data/jdbc/mapping/event/Unset.java new file mode 100644 index 0000000000..0fd30e4030 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/Unset.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import java.util.Optional; + +/** + * An unset identifier. Always returns {@link Optional#empty()} as value. + * + * @author Jens Schaude + * @author Oliver Gierke + * @since 2.0 + */ +enum Unset implements Identifier { + + UNSET; + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.mapping.event.Identifier#getOptionalValue() + */ + @Override + public Optional getOptionalValue() { + return Optional.empty(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/WithEntity.java b/src/main/java/org/springframework/data/jdbc/mapping/event/WithEntity.java new file mode 100644 index 0000000000..568a84376f --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/WithEntity.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +/** + * Interface for {@link SimpleJdbcEvent}s which are guaranteed to have an entity. Allows direct access to that entity, + * without going through an {@link java.util.Optional} + * + * @author Jens Schauder + * @since 2.0 + */ +public interface WithEntity extends JdbcEvent { + + /** + * @return will never be {@literal null}. + */ + default Object getEntity() { + return getOptionalEntity().orElseThrow(() -> new IllegalStateException("Entity must not be NULL")); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/WithId.java b/src/main/java/org/springframework/data/jdbc/mapping/event/WithId.java new file mode 100644 index 0000000000..f5c857dcc0 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/WithId.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.event; + +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; + +/** + * Interface for {@link SimpleJdbcEvent}s which are guaranteed to have a {@link Specified} identifier. Offers direct + * access to the {@link Specified} identifier. + * + * @author Jens Schauder + * @since 2.0 + */ +public interface WithId extends JdbcEvent { + + /** + * Events with an identifier will always return a {@link Specified} one. + */ + Specified getId(); +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentProperty.java b/src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentProperty.java new file mode 100644 index 0000000000..cda2801930 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentProperty.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.model; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; +import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.SimpleTypeHolder; + +/** + * Meta data about a property to be used by repository implementations. + * + * @author Jens Schauder + * @since 2.0 + */ +public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProperty + implements JdbcPersistentProperty { + + /** + * Creates a new {@link AnnotationBasedPersistentProperty}. + * + * @param property must not be {@literal null}. + * @param owner must not be {@literal null}. + * @param simpleTypeHolder must not be {@literal null}. + */ + public BasicJdbcPersistentProperty(Property property, PersistentEntity owner, + SimpleTypeHolder simpleTypeHolder) { + super(property, owner, simpleTypeHolder); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.AbstractPersistentProperty#createAssociation() + */ + @Override + protected Association createAssociation() { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty#getColumnName() + */ + public String getColumnName() { + return getName(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java new file mode 100644 index 0000000000..ee7a75d340 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.model; + +import org.springframework.data.mapping.PersistentEntity; + +/** + * @author Jens Schauder + * @author Oliver Gierke + * @since 2.0 + */ +public interface JdbcPersistentEntity extends PersistentEntity { + + /** + * Returns the name of the table backing the given entity. + * + * @return + */ + String getTableName(); + + /** + * Returns the column representing the identifier. + * + * @return will never be {@literal null}. + */ + String getIdColumn(); +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntityImpl.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntityImpl.java new file mode 100644 index 0000000000..744423f10b --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntityImpl.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.model; + +import lombok.Getter; + +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.util.TypeInformation; + +/** + * Meta data a repository might need for implementing persistence operations for instances of type {@code T} + * + * @author Jens Schauder + * @since 2.0 + */ +public class JdbcPersistentEntityImpl extends BasicPersistentEntity + implements JdbcPersistentEntity { + + private final @Getter String tableName; + + /** + * Creates a new {@link JdbcPersistentEntityImpl} for the given {@link TypeInformation}. + * + * @param information must not be {@literal null}. + */ + public JdbcPersistentEntityImpl(TypeInformation information) { + + super(information); + + tableName = getType().getSimpleName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity#getIdColumn() + */ + @Override + public String getIdColumn() { + return getRequiredIdProperty().getName(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java new file mode 100644 index 0000000000..7a8ffc634e --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.mapping.model; + +import org.springframework.data.mapping.PersistentProperty; + +/** + * A {@link PersistentProperty} for JDBC. + * + * @author Jens Schauder + * @author Oliver Gierke + * @since 2.0 + */ +public interface JdbcPersistentProperty extends PersistentProperty { + + /** + * Returns the name of the column backing that property. + * + * @return + */ + String getColumnName(); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java new file mode 100644 index 0000000000..b529d9187a --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java @@ -0,0 +1,109 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository; + +import lombok.RequiredArgsConstructor; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.convert.ClassGeneratingEntityInstantiator; +import org.springframework.data.convert.EntityInstantiator; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.jdbc.core.RowMapper; + +/** + * Maps a ResultSet to an entity of type {@code T} + * + * @author Jens Schauder + * @author Oliver Gierke + * @since 2.0 + */ +@RequiredArgsConstructor +class EntityRowMapper implements RowMapper { + + private final JdbcPersistentEntity entity; + private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator(); + private final ConversionService conversions = new DefaultConversionService(); + + /* + * (non-Javadoc) + * @see org.springframework.jdbc.core.RowMapper#mapRow(java.sql.ResultSet, int) + */ + @Override + public T mapRow(ResultSet resultSet, int rowNumber) throws SQLException { + + T result = createInstance(resultSet); + + entity.doWithProperties((PropertyHandler) property -> { + + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(result); + ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions); + + propertyAccessor.setProperty(property, readFrom(resultSet, property)); + }); + + return result; + } + + private T createInstance(ResultSet rs) { + return instantiator.createInstance(entity, ResultSetParameterValueProvider.of(rs, conversions)); + } + + private static Optional readFrom(ResultSet resultSet, PersistentProperty property) { + + try { + return Optional.ofNullable(resultSet.getObject(property.getName())); + } catch (SQLException o_O) { + throw new MappingException(String.format("Could not read property %s from result set!", property), o_O); + } + } + + @RequiredArgsConstructor(staticName = "of") + private static class ResultSetParameterValueProvider implements ParameterValueProvider { + + private final ResultSet resultSet; + private final ConversionService conversionService; + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter) + */ + @Override + public Optional getParameterValue(Parameter parameter) { + + return parameter.getName().map(name -> { + + try { + return conversionService.convert(resultSet.getObject(name), parameter.getType().getType()); + } catch (SQLException o_O) { + throw new MappingException(String.format("Couldn't read column %s from ResultSet.", name), o_O); + } + }); + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapper.java new file mode 100644 index 0000000000..24723ff04a --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapper.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.jdbc.mapping.event.AfterCreation; +import org.springframework.data.jdbc.mapping.event.Identifier; +import org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation; +import org.springframework.jdbc.core.RowMapper; + +/** + * A {@link RowMapper} that publishes events after a delegate, did the actual work of mapping a {@link ResultSet} to an + * entityInformation. + * + * @author Jens Schauder + * @since 2.0 + */ +@RequiredArgsConstructor +public class EventPublishingEntityRowMapper implements RowMapper { + + private final @NonNull RowMapper delegate; + private final @NonNull JdbcPersistentEntityInformation entityInformation; + private final @NonNull ApplicationEventPublisher publisher; + + /* + * (non-Javadoc) + * @see org.springframework.jdbc.core.RowMapper#mapRow(java.sql.ResultSet, int) + */ + @Override + public T mapRow(ResultSet resultSet, int i) throws SQLException { + + T instance = delegate.mapRow(resultSet, i); + + publisher.publishEvent(new AfterCreation(Identifier.of(entityInformation.getRequiredId(instance)), instance)); + + return instance; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java new file mode 100644 index 0000000000..db0e9cf4bf --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java @@ -0,0 +1,300 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.NonTransientDataAccessException; +import org.springframework.data.jdbc.mapping.event.AfterDelete; +import org.springframework.data.jdbc.mapping.event.AfterInsert; +import org.springframework.data.jdbc.mapping.event.AfterUpdate; +import org.springframework.data.jdbc.mapping.event.BeforeDelete; +import org.springframework.data.jdbc.mapping.event.BeforeInsert; +import org.springframework.data.jdbc.mapping.event.BeforeUpdate; +import org.springframework.data.jdbc.mapping.event.Identifier; +import org.springframework.data.jdbc.mapping.event.Identifier.Specified; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityImpl; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; +import org.springframework.data.jdbc.repository.support.BasicJdbcPersistentEntityInformation; +import org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.util.Streamable; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.util.Assert; + +/** + * @author Jens Schauder + * @since 2.0 + */ +public class SimpleJdbcRepository implements CrudRepository { + + private static final String ENTITY_NEW_AFTER_INSERT = "Entity [%s] still 'new' after insert. Please set either" + + " the id property in a before insert event handler, or ensure the database creates a value and your " + + "JDBC driver returns it."; + + private final JdbcPersistentEntity persistentEntity; + private final JdbcPersistentEntityInformation entityInformation; + private final NamedParameterJdbcOperations operations; + private final SqlGenerator sql; + private final EntityRowMapper entityRowMapper; + private final ApplicationEventPublisher publisher; + private final ConversionService conversions = new DefaultConversionService(); + + /** + * Creates a new {@link SimpleJdbcRepository} for the given {@link JdbcPersistentEntityImpl} + * + * @param persistentEntity + * @param jdbcOperations + * @param publisher + */ + public SimpleJdbcRepository(JdbcPersistentEntity persistentEntity, NamedParameterJdbcOperations jdbcOperations, + ApplicationEventPublisher publisher) { + + Assert.notNull(persistentEntity, "PersistentEntity must not be null."); + Assert.notNull(jdbcOperations, "JdbcOperations must not be null."); + Assert.notNull(publisher, "Publisher must not be null."); + + this.persistentEntity = persistentEntity; + this.entityInformation = new BasicJdbcPersistentEntityInformation<>(persistentEntity); + this.operations = jdbcOperations; + this.publisher = publisher; + + this.entityRowMapper = new EntityRowMapper<>(persistentEntity); + this.sql = new SqlGenerator(persistentEntity); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#save(S) + */ + @Override + public S save(S instance) { + + if (entityInformation.isNew(instance)) { + doInsert(instance); + } else { + doUpdate(instance); + } + + return instance; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) + */ + @Override + public Iterable saveAll(Iterable entities) { + + List savedEntities = new ArrayList<>(); + entities.forEach(e -> savedEntities.add(save(e))); + return savedEntities; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable) + */ + @Override + public Optional findById(ID id) { + + return Optional + .ofNullable(operations.queryForObject(sql.getFindOne(), new MapSqlParameterSource("id", id), entityRowMapper)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#exists(java.io.Serializable) + */ + @Override + public boolean existsById(ID id) { + return operations.queryForObject(sql.getExists(), new MapSqlParameterSource("id", id), Boolean.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#findAll() + */ + @Override + public Iterable findAll() { + return operations.query(sql.getFindAll(), entityRowMapper); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) + */ + @Override + public Iterable findAllById(Iterable ids) { + return operations.query(sql.getFindAllInList(), new MapSqlParameterSource("ids", ids), entityRowMapper); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#count() + */ + @Override + public long count() { + return operations.getJdbcOperations().queryForObject(sql.getCount(), Long.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) + */ + @Override + public void deleteById(ID id) { + doDelete(Identifier.of(id), Optional.empty()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object) + */ + @Override + public void delete(T instance) { + doDelete(Identifier.of(entityInformation.getRequiredId(instance)), Optional.of(instance)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable) + */ + @Override + public void deleteAll(Iterable entities) { + + List idList = Streamable.of(entities).stream() // + .map(e -> entityInformation.getRequiredId(e)) // + .collect(Collectors.toList()); + + MapSqlParameterSource sqlParameterSource = new MapSqlParameterSource("ids", idList); + operations.update(sql.getDeleteByList(), sqlParameterSource); + } + + @Override + public void deleteAll() { + operations.getJdbcOperations().update(sql.getDeleteAll()); + } + + private Map getPropertyMap(final S instance) { + + Map parameters = new HashMap<>(); + + this.persistentEntity.doWithProperties((PropertyHandler) property -> { + + Optional value = persistentEntity.getPropertyAccessor(instance).getProperty(property); + parameters.put(property.getColumnName(), value.orElse(null)); + }); + + return parameters; + } + + private void doInsert(S instance) { + + publisher.publishEvent(new BeforeInsert(instance)); + + KeyHolder holder = new GeneratedKeyHolder(); + + Map propertyMap = getPropertyMap(instance); + propertyMap.put(persistentEntity.getRequiredIdProperty().getColumnName(), getIdValueOrNull(instance)); + + operations.update(sql.getInsert(), new MapSqlParameterSource(propertyMap), holder); + setIdFromJdbc(instance, holder); + + if (entityInformation.isNew(instance)) { + throw new IllegalStateException(String.format(ENTITY_NEW_AFTER_INSERT, persistentEntity)); + } + + publisher.publishEvent(new AfterInsert(Identifier.of(entityInformation.getRequiredId(instance)), instance)); + } + + private ID getIdValueOrNull(S instance) { + + Optional idValue = entityInformation.getId(instance); + return isIdPropertySimpleTypeAndValueZero(idValue) ? null : idValue.get(); + } + + private boolean isIdPropertySimpleTypeAndValueZero(Optional idValue) { + + Optional idProperty = persistentEntity.getIdProperty(); + return !idValue.isPresent() // + || !idProperty.isPresent() // + || (((Optional) idProperty).get().getType() == int.class && idValue.equals(0)) // + || (((Optional) idProperty).get().getType() == long.class && idValue.equals(0L)); + } + + private void setIdFromJdbc(S instance, KeyHolder holder) { + + try { + + getIdFromHolder(holder).ifPresent(it -> { + + Class targetType = persistentEntity.getRequiredIdProperty().getType(); + Object converted = convert(it, targetType); + entityInformation.setId(instance, Optional.of(converted)); + }); + + } catch (NonTransientDataAccessException e) { + throw new UnableToSetId("Unable to set id of " + instance, e); + } + } + + private Optional getIdFromHolder(KeyHolder holder) { + + try { + // MySQL just returns one value with a special name + return Optional.ofNullable(holder.getKey()); + } catch (InvalidDataAccessApiUsageException e) { + // Postgres returns a value for each column + return Optional.ofNullable(holder.getKeys().get(persistentEntity.getIdColumn())); + } + + } + + private V convert(Object idValueFromJdbc, Class targetType) { + return conversions.convert(idValueFromJdbc, targetType); + } + + private void doDelete(Specified specifiedId, Optional optionalEntity) { + + publisher.publishEvent(new BeforeDelete(specifiedId, optionalEntity)); + operations.update(sql.getDeleteById(), new MapSqlParameterSource("id", specifiedId.getValue())); + publisher.publishEvent(new AfterDelete(specifiedId, optionalEntity)); + } + + private void doUpdate(S instance) { + + Specified specifiedId = Identifier.of(entityInformation.getRequiredId(instance)); + publisher.publishEvent(new BeforeUpdate(specifiedId, instance)); + operations.update(sql.getUpdate(), getPropertyMap(instance)); + publisher.publishEvent(new AfterUpdate(specifiedId, instance)); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java b/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java new file mode 100644 index 0000000000..89ad5918d6 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java @@ -0,0 +1,176 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; +import org.springframework.data.mapping.PropertyHandler; + +/** + * Generates SQL statements to be used by {@Link SimpleJdbcRepository} + * + * @author Jens Schauder + * @since 2.0 + */ +class SqlGenerator { + + private final String findOneSql; + private final String findAllSql; + private final String findAllInListSql; + + private final String existsSql; + private final String countSql; + + private final String insertSql; + + private final String updateSql; + + private final String deleteByIdSql; + private final String deleteAllSql; + private final String deleteByListSql; + + private final JdbcPersistentEntity entity; + private final List propertyNames = new ArrayList<>(); + private final List nonIdPropertyNames = new ArrayList<>(); + + SqlGenerator(JdbcPersistentEntity entity) { + + this.entity = entity; + + initPropertyNames(); + + findOneSql = createFindOneSelectSql(); + findAllSql = createFindAllSql(); + findAllInListSql = createFindAllInListSql(); + + existsSql = createExistsSql(); + countSql = createCountSql(); + + insertSql = createInsertSql(); + + updateSql = createUpdateSql(); + + deleteByIdSql = createDeleteSql(); + deleteAllSql = createDeleteAllSql(); + deleteByListSql = createDeleteByListSql(); + } + + private void initPropertyNames() { + + entity.doWithProperties((PropertyHandler) p -> { + propertyNames.add(p.getName()); + if (!entity.isIdProperty(p)) { + nonIdPropertyNames.add(p.getName()); + } + }); + } + + String getFindAllInList() { + return findAllInListSql; + } + + String getFindAll() { + return findAllSql; + } + + String getExists() { + return existsSql; + } + + String getFindOne() { + return findOneSql; + } + + String getInsert() { + return insertSql; + } + + String getUpdate() { + return updateSql; + } + + String getCount() { + return countSql; + } + + String getDeleteById() { + return deleteByIdSql; + } + + String getDeleteAll() { + return deleteAllSql; + } + + String getDeleteByList() { + return deleteByListSql; + } + + private String createFindOneSelectSql() { + return String.format("select * from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); + } + + private String createFindAllSql() { + return String.format("select * from %s", entity.getTableName()); + } + + private String createFindAllInListSql() { + return String.format("select * from %s where %s in (:ids)", entity.getTableName(), entity.getIdColumn()); + } + + private String createExistsSql() { + return String.format("select count(*) from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); + } + + private String createCountSql() { + return String.format("select count(*) from %s", entity.getTableName()); + } + + private String createInsertSql() { + + String insertTemplate = "insert into %s (%s) values (%s)"; + String tableColumns = String.join(", ", nonIdPropertyNames); + String parameterNames = nonIdPropertyNames.stream().collect(Collectors.joining(", :", ":", "")); + + return String.format(insertTemplate, entity.getTableName(), tableColumns, parameterNames); + } + + private String createUpdateSql() { + + String updateTemplate = "update %s set %s where %s = :%s"; + + String setClause = propertyNames.stream()// + .map(n -> String.format("%s = :%s", n, n))// + .collect(Collectors.joining(", ")); + + return String.format(updateTemplate, entity.getTableName(), setClause, entity.getIdColumn(), entity.getIdColumn()); + } + + private String createDeleteSql() { + return String.format("delete from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); + } + + private String createDeleteAllSql() { + return String.format("delete from %s", entity.getTableName()); + } + + private String createDeleteByListSql() { + return String.format("delete from %s where %s in (:ids)", entity.getTableName(), entity.getIdColumn()); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/UnableToSetId.java b/src/main/java/org/springframework/data/jdbc/repository/UnableToSetId.java new file mode 100644 index 0000000000..e6e22c1bc9 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/UnableToSetId.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository; + +import org.springframework.dao.NonTransientDataAccessException; + +/** + * Signals failure to set the id property of an entity. + * + * @author Jens Schauder + * @since 2.0 + */ +public class UnableToSetId extends NonTransientDataAccessException { + + private static final long serialVersionUID = 3285001352389420376L; + + public UnableToSetId(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/BasicJdbcPersistentEntityInformation.java b/src/main/java/org/springframework/data/jdbc/repository/support/BasicJdbcPersistentEntityInformation.java new file mode 100644 index 0000000000..6e994c3315 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/BasicJdbcPersistentEntityInformation.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository.support; + +import java.io.Serializable; +import java.util.Optional; + +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.repository.core.support.PersistentEntityInformation; + +/** + * @author Jens Schauder + * @since 2.0 + */ +public class BasicJdbcPersistentEntityInformation extends PersistentEntityInformation + implements JdbcPersistentEntityInformation { + + private final JdbcPersistentEntity persistentEntity; + + public BasicJdbcPersistentEntityInformation(JdbcPersistentEntity persistentEntity) { + + super(persistentEntity); + + this.persistentEntity = persistentEntity; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation#setId(java.lang.Object, java.util.Optional) + */ + @Override + public void setId(T instance, Optional value) { + persistentEntity.getPropertyAccessor(instance).setProperty(persistentEntity.getRequiredIdProperty(), value); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcPersistentEntityInformation.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcPersistentEntityInformation.java new file mode 100644 index 0000000000..b1936fa152 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcPersistentEntityInformation.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository.support; + +import java.io.Serializable; +import java.util.Optional; + +import org.springframework.data.repository.core.EntityInformation; + +/** + * @author Jens Schauder + * @since 2.0 + */ +public interface JdbcPersistentEntityInformation extends EntityInformation { + + void setId(T instance, Optional value); + + /** + * Returns the identifier of the given entity or throws and exception if it can't be obtained. + * + * @param entity must not be {@literal null}. + * @return the identifier of the given entity + * @throws IllegalArgumentException in case no identifier can be obtained for the given entity. + */ + default ID getRequiredId(T entity) { + return getId(entity).orElseThrow(() -> new IllegalArgumentException( + String.format("Could not obtain required identifier from entity %s!", entity))); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java new file mode 100644 index 0000000000..be2160d067 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository.support; + +import lombok.RequiredArgsConstructor; + +import java.io.Serializable; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.jdbc.mapping.context.JdbcMappingContext; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.jdbc.repository.SimpleJdbcRepository; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; + +/** + * @author Jens Schauder + * @since 2.0 + */ +@RequiredArgsConstructor +public class JdbcRepositoryFactory extends RepositoryFactorySupport { + + private final JdbcMappingContext context = new JdbcMappingContext(); + private final NamedParameterJdbcOperations jdbcOperations; + private final ApplicationEventPublisher publisher; + + @SuppressWarnings("unchecked") + @Override + public EntityInformation getEntityInformation(Class aClass) { + + return context.getPersistentEntity(aClass) + .map(e -> new BasicJdbcPersistentEntityInformation((JdbcPersistentEntity) e)).orElseGet(null); + } + + @Override + protected Object getTargetRepository(RepositoryInformation repositoryInformation) { + + JdbcPersistentEntity persistentEntity = context // + .getPersistentEntity(repositoryInformation.getDomainType()) // + .orElseThrow(() -> new IllegalArgumentException("%s does not represent a persistent entity")); // + return new SimpleJdbcRepository<>(persistentEntity, jdbcOperations, publisher); + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata repositoryMetadata) { + return SimpleJdbcRepository.class; + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperTest.java b/src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperTest.java new file mode 100644 index 0000000000..fe6f94bad7 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import lombok.Value; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.mapping.event.AfterCreation; +import org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation; +import org.springframework.jdbc.core.RowMapper; + +/** + * Unit tests for {@link EventPublishingEntityRowMapper}. + * + * @author Jens Schauder + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class EventPublishingEntityRowMapperTest { + + @Mock RowMapper rowMapperDelegate; + @Mock JdbcPersistentEntityInformation entityInformation; + @Mock ApplicationEventPublisher publisher; + + @Test // DATAJDBC-99 + public void eventGetsPublishedAfterInstantiation() throws SQLException { + + when(rowMapperDelegate.mapRow(any(ResultSet.class), anyInt())).thenReturn(new DummyEntity(1L)); + when(entityInformation.getId(any())).thenReturn(Optional.of(1L)); + + EventPublishingEntityRowMapper rowMapper = new EventPublishingEntityRowMapper<>(rowMapperDelegate, + entityInformation, publisher); + + ResultSet resultSet = mock(ResultSet.class); + rowMapper.mapRow(resultSet, 1); + + verify(publisher).publishEvent(isA(AfterCreation.class)); + } + + @Value + static class DummyEntity { + @Id Long Id; + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java new file mode 100644 index 0000000000..2c0ab66bbc --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository; + +import static org.assertj.core.api.Assertions.*; + +import lombok.Data; +import lombok.Value; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.rules.SpringClassRule; +import org.springframework.test.context.junit4.rules.SpringMethodRule; + +/** + * Testing special cases for id generation with {@link SimpleJdbcRepository}. + * + * @author Jens Schauder + */ +@ContextConfiguration +public class JdbcRepositoryIdGenerationIntegrationTests { + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Autowired JdbcRepositoryFactory factory; + + @Bean + Class testClass() { + return JdbcRepositoryIdGenerationIntegrationTests.class; + } + + @Bean + ReadOnlyIdEntityRepository readOnlyIdRepository() { + return factory.getRepository(ReadOnlyIdEntityRepository.class); + } + + @Bean + PrimitiveIdEntityRepository primitiveIdRepository() { + return factory.getRepository(PrimitiveIdEntityRepository.class); + } + } + + @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); + @Rule public SpringMethodRule methodRule = new SpringMethodRule(); + + @Autowired NamedParameterJdbcTemplate template; + @Autowired ReadOnlyIdEntityRepository readOnlyIdrepository; + @Autowired PrimitiveIdEntityRepository primitiveIdRepository; + + @Test // DATAJDBC-98 + public void idWithoutSetterGetsSet() { + + ReadOnlyIdEntity entity = readOnlyIdrepository.save(new ReadOnlyIdEntity(null, "Entity Name")); + + assertThat(entity.getId()).isNotNull(); + + assertThat(readOnlyIdrepository.findById(entity.getId())).hasValueSatisfying(it -> { + + assertThat(it.getId()).isEqualTo(entity.getId()); + assertThat(it.getName()).isEqualTo(entity.getName()); + }); + } + + @Test // DATAJDBC-98 + public void primitiveIdGetsSet() { + + PrimitiveIdEntity entity = new PrimitiveIdEntity(0); + entity.setName("Entity Name"); + + PrimitiveIdEntity saved = primitiveIdRepository.save(entity); + + assertThat(saved.getId()).isNotEqualTo(0L); + + assertThat(primitiveIdRepository.findById(saved.getId())).hasValueSatisfying(it -> { + + assertThat(it.getId()).isEqualTo(saved.getId()); + assertThat(it.getName()).isEqualTo(saved.getName()); + }); + } + + private interface PrimitiveIdEntityRepository extends CrudRepository {} + + public interface ReadOnlyIdEntityRepository extends CrudRepository {} + + @Value + static class ReadOnlyIdEntity { + + @Id Long id; + String name; + } + + @Data + static class PrimitiveIdEntity { + + @Id private final long id; + String name; + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java new file mode 100644 index 0000000000..c7e9d67d5e --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -0,0 +1,251 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.repository; + +import static java.util.Arrays.*; +import static org.assertj.core.api.Assertions.*; + +import lombok.Data; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.rules.SpringClassRule; +import org.springframework.test.context.junit4.rules.SpringMethodRule; +import org.springframework.test.jdbc.JdbcTestUtils; +import org.springframework.transaction.annotation.Transactional; + +/** + * Very simple use cases for creation and usage of JdbcRepositories. + * + * @author Jens Schauder + */ +@ContextConfiguration +@Transactional +public class JdbcRepositoryIntegrationTests { + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Autowired JdbcRepositoryFactory factory; + + @Bean + Class testClass() { + return JdbcRepositoryIntegrationTests.class; + } + + @Bean + DummyEntityRepository dummyEntityRepository() { + return factory.getRepository(DummyEntityRepository.class); + } + } + + @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); + @Rule public SpringMethodRule methodRule = new SpringMethodRule(); + + @Autowired NamedParameterJdbcTemplate template; + @Autowired DummyEntityRepository repository; + + @Test // DATAJDBC-95 + public void savesAnEntity() { + + DummyEntity entity = repository.save(createDummyEntity()); + + assertThat(JdbcTestUtils.countRowsInTableWhere((JdbcTemplate) template.getJdbcOperations(), "dummyentity", + "idProp = " + entity.getIdProp())).isEqualTo(1); + } + + @Test // DATAJDBC-95 + public void saveAndLoadAnEntity() { + + DummyEntity entity = repository.save(createDummyEntity()); + + assertThat(repository.findById(entity.getIdProp())).hasValueSatisfying(it -> { + + assertThat(it.getIdProp()).isEqualTo(entity.getIdProp()); + assertThat(it.getName()).isEqualTo(entity.getName()); + }); + } + + @Test // DATAJDBC-97 + public void savesManyEntities() { + + DummyEntity entity = createDummyEntity(); + DummyEntity other = createDummyEntity(); + + repository.saveAll(asList(entity, other)); + + assertThat(repository.findAll()) // + .extracting(DummyEntity::getIdProp) // + .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); + } + + @Test // DATAJDBC-97 + public void existsReturnsTrueIffEntityExists() { + + DummyEntity entity = repository.save(createDummyEntity()); + + assertThat(repository.existsById(entity.getIdProp())).isTrue(); + assertThat(repository.existsById(entity.getIdProp() + 1)).isFalse(); + } + + @Test // DATAJDBC-97 + public void findAllFindsAllEntities() { + + DummyEntity entity = repository.save(createDummyEntity()); + DummyEntity other = repository.save(createDummyEntity()); + + Iterable all = repository.findAll(); + + assertThat(all)// + .extracting(DummyEntity::getIdProp)// + .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); + } + + @Test // DATAJDBC-97 + public void findAllFindsAllSpecifiedEntities() { + + DummyEntity entity = repository.save(createDummyEntity()); + DummyEntity other = repository.save(createDummyEntity()); + + assertThat(repository.findAllById(asList(entity.getIdProp(), other.getIdProp())))// + .extracting(DummyEntity::getIdProp)// + .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); + } + + @Test // DATAJDBC-97 + public void countsEntities() { + + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); + + assertThat(repository.count()).isEqualTo(3L); + } + + @Test // DATAJDBC-97 + public void deleteById() { + + DummyEntity one = repository.save(createDummyEntity()); + DummyEntity two = repository.save(createDummyEntity()); + DummyEntity three = repository.save(createDummyEntity()); + + repository.deleteById(two.getIdProp()); + + assertThat(repository.findAll()) // + .extracting(DummyEntity::getIdProp) // + .containsExactlyInAnyOrder(one.getIdProp(), three.getIdProp()); + } + + @Test // DATAJDBC-97 + public void deleteByEntity() { + + DummyEntity one = repository.save(createDummyEntity()); + DummyEntity two = repository.save(createDummyEntity()); + DummyEntity three = repository.save(createDummyEntity()); + + repository.delete(one); + + assertThat(repository.findAll()) // + .extracting(DummyEntity::getIdProp) // + .containsExactlyInAnyOrder(two.getIdProp(), three.getIdProp()); + } + + @Test // DATAJDBC-97 + public void deleteByList() { + + DummyEntity one = repository.save(createDummyEntity()); + DummyEntity two = repository.save(createDummyEntity()); + DummyEntity three = repository.save(createDummyEntity()); + + repository.deleteAll(asList(one, three)); + + assertThat(repository.findAll()) // + .extracting(DummyEntity::getIdProp) // + .containsExactlyInAnyOrder(two.getIdProp()); + } + + @Test // DATAJDBC-97 + public void deleteAll() { + + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); + + assertThat(repository.findAll()).isNotEmpty(); + + repository.deleteAll(); + + assertThat(repository.findAll()).isEmpty(); + } + + @Test // DATAJDBC-98 + public void update() { + + DummyEntity entity = repository.save(createDummyEntity()); + + entity.setName("something else"); + DummyEntity saved = repository.save(entity); + + assertThat(repository.findById(entity.getIdProp())).hasValueSatisfying(it -> { + assertThat(it.getName()).isEqualTo(saved.getName()); + }); + } + + @Test // DATAJDBC-98 + public void updateMany() { + + DummyEntity entity = repository.save(createDummyEntity()); + DummyEntity other = repository.save(createDummyEntity()); + + entity.setName("something else"); + other.setName("others Name"); + + repository.saveAll(asList(entity, other)); + + assertThat(repository.findAll()) // + .extracting(DummyEntity::getName) // + .containsExactlyInAnyOrder(entity.getName(), other.getName()); + } + + private static DummyEntity createDummyEntity() { + + DummyEntity entity = new DummyEntity(); + entity.setName("Entity Name"); + return entity; + } + + interface DummyEntityRepository extends CrudRepository {} + + @Data + static class DummyEntity { + + String name; + @Id private Long idProp; + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java new file mode 100644 index 0000000000..982152d019 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -0,0 +1,135 @@ +package org.springframework.data.jdbc.repository; + +import static java.util.Arrays.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.stubbing.Answer; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.mapping.event.AfterDelete; +import org.springframework.data.jdbc.mapping.event.AfterInsert; +import org.springframework.data.jdbc.mapping.event.AfterUpdate; +import org.springframework.data.jdbc.mapping.event.BeforeDelete; +import org.springframework.data.jdbc.mapping.event.BeforeInsert; +import org.springframework.data.jdbc.mapping.event.BeforeUpdate; +import org.springframework.data.jdbc.mapping.event.Identifier; +import org.springframework.data.jdbc.mapping.event.JdbcEvent; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.KeyHolder; + +/** + * @author Jens Schauder + */ +public class SimpleJdbcRepositoryEventsUnitTests { + + FakePublisher publisher = new FakePublisher(); + + DummyEntityRepository repository; + + @Before + public void before() { + + NamedParameterJdbcOperations operations = createIdGeneratingOperations(); + JdbcRepositoryFactory factory = new JdbcRepositoryFactory(operations, publisher); + repository = factory.getRepository(DummyEntityRepository.class); + } + + @Test // DATAJDBC-99 + public void publishesEventsOnSave() { + + DummyEntity entity = new DummyEntity(23L); + + repository.save(entity); + + assertThat(publisher.events.get(0)).isInstanceOf(BeforeUpdate.class); + assertThat(publisher.events.get(1)).isInstanceOf(AfterUpdate.class); + } + + @Test // DATAJDBC-99 + public void publishesEventsOnSaveMany() { + + DummyEntity entity1 = new DummyEntity(null); + DummyEntity entity2 = new DummyEntity(23L); + + repository.saveAll(asList(entity1, entity2)); + + assertThat(publisher.events.get(0)).isInstanceOf(BeforeInsert.class); + assertThat(publisher.events.get(1)).isInstanceOf(AfterInsert.class); + assertThat(publisher.events.get(2)).isInstanceOf(BeforeUpdate.class); + assertThat(publisher.events.get(3)).isInstanceOf(AfterUpdate.class); + } + + @Test // DATAJDBC-99 + public void publishesEventsOnDelete() { + + DummyEntity entity = new DummyEntity(23L); + + repository.delete(entity); + + assertThat(publisher.events.get(0)).isInstanceOf(BeforeDelete.class); + assertThat(publisher.events.get(1)).isInstanceOf(AfterDelete.class); + + assertThat(publisher.events.get(0).getOptionalEntity()).hasValue(entity); + assertThat(publisher.events.get(1).getOptionalEntity()).hasValue(entity); + + assertThat(publisher.events.get(0).getId()).isEqualTo(Identifier.of(23L)); + assertThat(publisher.events.get(1).getId()).isEqualTo(Identifier.of(23L)); + } + + @Test // DATAJDBC-99 + public void publishesEventsOnDeleteById() { + + repository.deleteById(23L); + + assertThat(publisher.events.get(0)).isInstanceOf(BeforeDelete.class); + assertThat(publisher.events.get(1)).isInstanceOf(AfterDelete.class); + } + + private static NamedParameterJdbcOperations createIdGeneratingOperations() { + + Answer setIdInKeyHolder = invocation -> { + + HashMap keys = new HashMap<>(); + keys.put("id", 4711L); + KeyHolder keyHolder = invocation.getArgument(2); + keyHolder.getKeyList().add(keys); + + return 1; + }; + + NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); + when(operations.update(anyString(), any(SqlParameterSource.class), any(KeyHolder.class))) + .thenAnswer(setIdInKeyHolder); + return operations; + } + + interface DummyEntityRepository extends CrudRepository {} + + @Data + static class DummyEntity { + private final @Id Long id; + } + + static class FakePublisher implements ApplicationEventPublisher { + + List events = new ArrayList<>(); + + @Override + public void publishEvent(Object o) { + events.add((JdbcEvent) o); + } + } +} diff --git a/src/test/java/org/springframework/data/jdbc/testing/DataSourceConfiguration.java b/src/test/java/org/springframework/data/jdbc/testing/DataSourceConfiguration.java new file mode 100644 index 0000000000..01db760c67 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/testing/DataSourceConfiguration.java @@ -0,0 +1,79 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.testing; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; + +/** + * Basic configuration expecting subclasses to provide a {@link DataSource} via {@link #createDataSource()} to be + * exposed to the {@link ApplicationContext}. + * + * @author Jens Schauder + * @author Oliver Gierke + */ +@Configuration +abstract class DataSourceConfiguration { + + @Autowired Class testClass; + @Autowired Environment environment; + + @Bean + DataSource dataSource() { + return createDataSource(); + } + + @Bean + DataSourceInitializer initializer() { + + DataSourceInitializer initializer = new DataSourceInitializer(); + initializer.setDataSource(dataSource()); + + String[] activeProfiles = environment.getActiveProfiles(); + String profile = activeProfiles.length == 0 ? "" : activeProfiles[0]; + + ClassPathResource script = new ClassPathResource(TestUtils.createScriptName(testClass, profile)); + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(script); + customizePopulator(populator); + initializer.setDatabasePopulator(populator); + + return initializer; + } + + /** + * Return the {@link DataSource} to be exposed as a Spring bean. + * + * @return + */ + protected abstract DataSource createDataSource(); + + /** + * Callback to customize the {@link ResourceDatabasePopulator} before it will be applied to the {@link DataSource}. It + * will be pre-populated with a SQL script derived from the name of the current test class and the activated Spring + * profile. + * + * @param populator will never be {@literal null}. + */ + protected void customizePopulator(ResourceDatabasePopulator populator) {} +} diff --git a/src/test/java/org/springframework/data/jdbc/testing/HsqlDataSourceConfiguration.java b/src/test/java/org/springframework/data/jdbc/testing/HsqlDataSourceConfiguration.java new file mode 100644 index 0000000000..0b92c63cb4 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/testing/HsqlDataSourceConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.testing; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +/** + * {@link DataSource} setup for HSQL. + * + * @author Jens Schauder + * @author Oliver Gierke + */ +@Configuration +@Profile({ "hsql", "default" }) +class HsqlDataSourceConfiguration { + + @Autowired Class context; + + @Bean + DataSource dataSource() { + + return new EmbeddedDatabaseBuilder() // + .generateUniqueName(true) // + .setType(EmbeddedDatabaseType.HSQL) // + .setScriptEncoding("UTF-8") // + .ignoreFailedDrops(true) // + .addScript(TestUtils.createScriptName(context, "hsql")) // + .build(); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java b/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java new file mode 100644 index 0000000000..2bd81a504f --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.testing; + +import javax.annotation.PostConstruct; +import javax.sql.DataSource; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.DatabasePopulator; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; + +import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; + +/** + * {@link DataSource} setup for MySQL. + * + * @author Jens Schauder + * @author Oliver Gierke + */ +@Configuration +@Profile("mysql") +class MySqlDataSourceConfiguration extends DataSourceConfiguration { + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.testing.DataSourceConfiguration#createDataSource() + */ + protected DataSource createDataSource() { + + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl("jdbc:mysql:///test?user=root"); + + return dataSource; + } + + @PostConstruct + public void initDatabase() { + + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl("jdbc:mysql:///?user=root"); + + ClassPathResource createScript = new ClassPathResource("create-mysql.sql"); + DatabasePopulator databasePopulator = new ResourceDatabasePopulator(createScript); + + DataSourceInitializer initializer = new DataSourceInitializer(); + initializer.setDatabasePopulator(databasePopulator); + initializer.setDataSource(dataSource); + initializer.afterPropertiesSet(); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/testing/PostgresDataSourceConfiguration.java b/src/test/java/org/springframework/data/jdbc/testing/PostgresDataSourceConfiguration.java new file mode 100644 index 0000000000..6a7f192500 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/testing/PostgresDataSourceConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.testing; + +import javax.sql.DataSource; + +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; + +/** + * {@link DataSource} setup for PostgreSQL + * + * @author Jens Schauder + * @author Oliver Gierke + */ +@Configuration +@Profile("postgres") +public class PostgresDataSourceConfiguration extends DataSourceConfiguration { + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.testing.DataSourceConfiguration#createDataSource() + */ + protected DataSource createDataSource() { + + PGSimpleDataSource ds = new PGSimpleDataSource(); + ds.setUrl("jdbc:postgresql:///postgres"); + + return ds; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.testing.DataSourceFactoryBean#customizePopulator(org.springframework.jdbc.datasource.init.ResourceDatabasePopulator) + */ + @Override + protected void customizePopulator(ResourceDatabasePopulator populator) { + populator.setIgnoreFailedDrops(true); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java new file mode 100644 index 0000000000..0c93cc3f6d --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.testing; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * Infrastructure configuration for integration tests. + * + * @author Oliver Gierke + */ +@Configuration +@ComponentScan // To pick up configuration classes (per activated profile) +public class TestConfiguration { + + @Autowired DataSource dataSource; + @Autowired ApplicationEventPublisher publisher; + + @Bean + JdbcRepositoryFactory jdbcRepositoryFactory() { + return new JdbcRepositoryFactory(namedParameterJdbcTemplate(), publisher); + } + + @Bean + NamedParameterJdbcTemplate namedParameterJdbcTemplate() { + return new NamedParameterJdbcTemplate(dataSource); + } + + @Bean + PlatformTransactionManager transactionManager() { + return new DataSourceTransactionManager(dataSource); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/testing/TestUtils.java b/src/test/java/org/springframework/data/jdbc/testing/TestUtils.java new file mode 100644 index 0000000000..e9e1a5dc01 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/testing/TestUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017 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 org.springframework.data.jdbc.testing; + +import org.springframework.util.Assert; + +/** + * Utility methods for testing. + * + * @author Oliver Gierke + */ +public interface TestUtils { + + /** + * Returns the name of the SQL script to be loaded for the given test class and database type. + * + * @param testClass must not be {@literal null}. + * @param databaseType must not be {@literal null} or empty. + * @return + */ + public static String createScriptName(Class testClass, String databaseType) { + + Assert.notNull(testClass, "Test class must not be null!"); + Assert.hasText(databaseType, "Database type must not be null or empty!"); + + return String.format("%s/%s-%s.sql", testClass.getPackage().getName(), testClass.getSimpleName(), + databaseType.toLowerCase()); + } +} diff --git a/src/test/resources/create-mysql.sql b/src/test/resources/create-mysql.sql new file mode 100644 index 0000000000..f1afbb0e10 --- /dev/null +++ b/src/test/resources/create-mysql.sql @@ -0,0 +1,2 @@ +DROP DATABASE test; +CREATE DATABASE test; diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000000..ad5cbef50a --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-hsql.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-hsql.sql new file mode 100644 index 0000000000..1618e2569b --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-hsql.sql @@ -0,0 +1,4 @@ +-- noinspection SqlNoDataSourceInspectionForFile + +CREATE TABLE ReadOnlyIdEntity (ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, NAME VARCHAR(100)) +CREATE TABLE PrimitiveIdEntity (ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, NAME VARCHAR(100)) diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-mysql.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-mysql.sql new file mode 100644 index 0000000000..b02242b2c9 --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-mysql.sql @@ -0,0 +1,2 @@ +CREATE TABLE ReadOnlyIdEntity (ID BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE PrimitiveIdEntity (ID BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-postgres.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-postgres.sql new file mode 100644 index 0000000000..267f0b7aba --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIdGenerationIntegrationTests-postgres.sql @@ -0,0 +1,5 @@ +DROP TABLE ReadOnlyIdEntity; +DROP TABLE PrimitiveIdEntity; + +CREATE TABLE ReadOnlyIdEntity (ID SERIAL PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE PrimitiveIdEntity (ID SERIAL PRIMARY KEY, NAME VARCHAR(100)); diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql new file mode 100644 index 0000000000..d6458976a7 --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -0,0 +1 @@ +CREATE TABLE dummyentity ( idProp BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100)) diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql new file mode 100644 index 0000000000..89763ecdbf --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -0,0 +1 @@ +CREATE TABLE dummyentity (idProp BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql new file mode 100644 index 0000000000..25e9c426ae --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -0,0 +1,2 @@ +DROP TABLE dummyentity; +CREATE TABLE dummyentity (idProp SERIAL PRIMARY KEY, NAME VARCHAR(100)); diff --git a/start-all-dbs.sh b/start-all-dbs.sh new file mode 100755 index 0000000000..69ef5d777e --- /dev/null +++ b/start-all-dbs.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# postgres +pg_ctl -D /usr/local/var/postgres start + +# mysql +mysql.server start diff --git a/stop-all-dbs.sh b/stop-all-dbs.sh new file mode 100755 index 0000000000..402d21c35d --- /dev/null +++ b/stop-all-dbs.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# postgres +pg_ctl -D /usr/local/var/postgres stop + +# mysql +mysql.server stop